summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/toaster/toastergui
diff options
context:
space:
mode:
authorDavid Reyna <David.Reyna@windriver.com>2018-08-15 18:04:09 -0700
committerRichard Purdie <richard.purdie@linuxfoundation.org>2018-08-20 10:20:51 +0100
commit08fbdc02e32d715ae94e3b9603fb3ec8351c8fd3 (patch)
tree8b1eb6531f782ba531903cc7248fbd5705e5adf3 /bitbake/lib/toaster/toastergui
parent9af0f1a46bbb6ad9ee8b35957251f4aa826b023f (diff)
downloadpoky-08fbdc02e32d715ae94e3b9603fb3ec8351c8fd3.tar.gz
bitbake: Toaster: Implement the project-specific feature and releated enhancements and defects.
Here is the primary driving enhancement: * Bug 12785 - Support Project Specific configuration for external tools (e.g. ISS, Eclipse) - Isolated project-specific configuration page (full Toaster context hidden) - Support for new project, reconfigure existing project, and import existing command line project - Ability to define variables (e.g. image recipe) and pass them back to external GUI - Ability to execute the cloning phase, so that external GUI receive a buildable project - Ability to call back to the external GUI when updates are completed and ready - Compatibility of above projects with the normal full Toaster interface - Ability to pass to a 'complete' or 'cancel' web page so that the external GUI can immediately stop that Toaster instance, and not leave dangling servers nor edit sessions open Here are the supporting enhancements, where at least the back end is implemented: * Bug 12821 - Make Toaster conf changes compatible with command line usage * Bug 12822 - Support importing user changes to conf files into Toaster * Bug 12823 - Support importing user build directories into Toaster * Bug 12824 - Scan imported layers for content so that they are immediately available * Bug 12825 - show layer clone item in progress bar Here are defects fixed: * Bug 12817 - builddelete.py requires explicit 'add_arguments' * Bug 12818 - Remove orphaned imported layers when project is deleted * Bug 12826 - fix imported layer management * Bug 12819 - build using selected bitbake env, not Toaster's env * Bug 12820 - Toaster randomizes the layer order in toaster_bblayers.conf [YOCTO #12785] (Bitbake rev: 985d6cec290bdd80998a63483561a73c75d82d65) Signed-off-by: David Reyna <David.Reyna@windriver.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake/lib/toaster/toastergui')
-rw-r--r--bitbake/lib/toaster/toastergui/api.py168
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/layerBtn.js12
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/libtoaster.js105
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/mrbsection.js4
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/projecttopbar.js22
-rw-r--r--bitbake/lib/toaster/toastergui/tables.py4
-rw-r--r--bitbake/lib/toaster/toastergui/templates/base_specific.html128
-rw-r--r--bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html48
-rw-r--r--bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html2
-rw-r--r--bitbake/lib/toaster/toastergui/templates/importlayer.html4
-rw-r--r--bitbake/lib/toaster/toastergui/templates/landing_specific.html50
-rw-r--r--bitbake/lib/toaster/toastergui/templates/layerdetails.html3
-rw-r--r--bitbake/lib/toaster/toastergui/templates/mrb_section.html2
-rw-r--r--bitbake/lib/toaster/toastergui/templates/newcustomimage.html4
-rw-r--r--bitbake/lib/toaster/toastergui/templates/newproject_specific.html95
-rw-r--r--bitbake/lib/toaster/toastergui/templates/project.html7
-rw-r--r--bitbake/lib/toaster/toastergui/templates/project_specific.html162
-rw-r--r--bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html80
-rw-r--r--bitbake/lib/toaster/toastergui/templates/projectconf.html7
-rw-r--r--bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html23
-rw-r--r--bitbake/lib/toaster/toastergui/urls.py13
-rw-r--r--[-rwxr-xr-x]bitbake/lib/toaster/toastergui/views.py151
-rw-r--r--bitbake/lib/toaster/toastergui/widgets.py6
23 files changed, 1086 insertions, 14 deletions
diff --git a/bitbake/lib/toaster/toastergui/api.py b/bitbake/lib/toaster/toastergui/api.py
index ab6ba69e0e..1bec56d468 100644
--- a/bitbake/lib/toaster/toastergui/api.py
+++ b/bitbake/lib/toaster/toastergui/api.py
@@ -22,7 +22,9 @@ import os
22import re 22import re
23import logging 23import logging
24import json 24import json
25import subprocess
25from collections import Counter 26from collections import Counter
27from shutil import copyfile
26 28
27from orm.models import Project, ProjectTarget, Build, Layer_Version 29from orm.models import Project, ProjectTarget, Build, Layer_Version
28from orm.models import LayerVersionDependency, LayerSource, ProjectLayer 30from orm.models import LayerVersionDependency, LayerSource, ProjectLayer
@@ -38,6 +40,18 @@ from django.core.urlresolvers import reverse
38from django.db.models import Q, F 40from django.db.models import Q, F
39from django.db import Error 41from django.db import Error
40from toastergui.templatetags.projecttags import filtered_filesizeformat 42from toastergui.templatetags.projecttags import filtered_filesizeformat
43from django.utils import timezone
44import pytz
45
46# development/debugging support
47verbose = 2
48def _log(msg):
49 if 1 == verbose:
50 print(msg)
51 elif 2 == verbose:
52 f1=open('/tmp/toaster.log', 'a')
53 f1.write("|" + msg + "|\n" )
54 f1.close()
41 55
42logger = logging.getLogger("toaster") 56logger = logging.getLogger("toaster")
43 57
@@ -137,6 +151,130 @@ class XhrBuildRequest(View):
137 return response 151 return response
138 152
139 153
154class XhrProjectUpdate(View):
155
156 def get(self, request, *args, **kwargs):
157 return HttpResponse()
158
159 def post(self, request, *args, **kwargs):
160 """
161 Project Update
162
163 Entry point: /xhr_projectupdate/<project_id>
164 Method: POST
165
166 Args:
167 pid: pid of project to update
168
169 Returns:
170 {"error": "ok"}
171 or
172 {"error": <error message>}
173 """
174
175 project = Project.objects.get(pk=kwargs['pid'])
176 logger.debug("ProjectUpdateCallback:project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
177
178 if 'do_update' in request.POST:
179
180 # Extract any default image recipe
181 if 'default_image' in request.POST:
182 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,str(request.POST['default_image']))
183 else:
184 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,'')
185
186 logger.debug("ProjectUpdateCallback:Chain to the build request")
187
188 # Chain to the build request
189 xhrBuildRequest = XhrBuildRequest()
190 return xhrBuildRequest.post(request, *args, **kwargs)
191
192 logger.warning("ERROR:XhrProjectUpdate")
193 response = HttpResponse()
194 response.status_code = 500
195 return response
196
197class XhrSetDefaultImageUrl(View):
198
199 def get(self, request, *args, **kwargs):
200 return HttpResponse()
201
202 def post(self, request, *args, **kwargs):
203 """
204 Project Update
205
206 Entry point: /xhr_setdefaultimage/<project_id>
207 Method: POST
208
209 Args:
210 pid: pid of project to update default image
211
212 Returns:
213 {"error": "ok"}
214 or
215 {"error": <error message>}
216 """
217
218 project = Project.objects.get(pk=kwargs['pid'])
219 logger.debug("XhrSetDefaultImageUrl:project.pk=%d" % (project.pk))
220
221 # set any default image recipe
222 if 'targets' in request.POST:
223 default_target = str(request.POST['targets'])
224 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,default_target)
225 logger.debug("XhrSetDefaultImageUrl,project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
226 return error_response('ok')
227
228 logger.warning("ERROR:XhrSetDefaultImageUrl")
229 response = HttpResponse()
230 response.status_code = 500
231 return response
232
233
234#
235# Layer Management
236#
237# Rules for 'local_source_dir' layers
238# * Layers must have a unique name in the Layers table
239# * A 'local_source_dir' layer is supposed to be shared
240# by all projects that use it, so that it can have the
241# same logical name
242# * Each project that uses a layer will have its own
243# LayerVersion and Project Layer for it
244# * During the Paroject delete process, when the last
245# LayerVersion for a 'local_source_dir' layer is deleted
246# then the Layer record is deleted to remove orphans
247#
248
249def scan_layer_content(layer,layer_version):
250 # if this is a local layer directory, we can immediately scan its content
251 if layer.local_source_dir:
252 try:
253 # recipes-*/*/*.bb
254 cmd = '%s %s' % ('ls', os.path.join(layer.local_source_dir,'recipes-*/*/*.bb'))
255 recipes_list = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.read()
256 recipes_list = recipes_list.decode("utf-8").strip()
257 if recipes_list and 'No such' not in recipes_list:
258 for recipe in recipes_list.split('\n'):
259 recipe_path = recipe[recipe.rfind('recipes-'):]
260 recipe_name = recipe[recipe.rfind('/')+1:].replace('.bb','')
261 recipe_ver = recipe_name.rfind('_')
262 if recipe_ver > 0:
263 recipe_name = recipe_name[0:recipe_ver]
264 if recipe_name:
265 ro, created = Recipe.objects.get_or_create(
266 layer_version=layer_version,
267 name=recipe_name
268 )
269 if created:
270 ro.file_path = recipe_path
271 ro.summary = 'Recipe %s from layer %s' % (recipe_name,layer.name)
272 ro.description = ro.summary
273 ro.save()
274
275 except Exception as e:
276 logger.warning("ERROR:scan_layer_content: %s" % e)
277
140class XhrLayer(View): 278class XhrLayer(View):
141 """ Delete, Get, Add and Update Layer information 279 """ Delete, Get, Add and Update Layer information
142 280
@@ -265,6 +403,7 @@ class XhrLayer(View):
265 (csv)] 403 (csv)]
266 404
267 """ 405 """
406
268 try: 407 try:
269 project = Project.objects.get(pk=kwargs['pid']) 408 project = Project.objects.get(pk=kwargs['pid'])
270 409
@@ -285,7 +424,13 @@ class XhrLayer(View):
285 if layer_data['name'] in existing_layers: 424 if layer_data['name'] in existing_layers:
286 return JsonResponse({"error": "layer-name-exists"}) 425 return JsonResponse({"error": "layer-name-exists"})
287 426
288 layer = Layer.objects.create(name=layer_data['name']) 427 if ('local_source_dir' in layer_data):
428 # Local layer can be shared across projects. They have no 'release'
429 # and are not included in get_all_compatible_layer_versions() above
430 layer,created = Layer.objects.get_or_create(name=layer_data['name'])
431 _log("Local Layer created=%s" % created)
432 else:
433 layer = Layer.objects.create(name=layer_data['name'])
289 434
290 layer_version = Layer_Version.objects.create( 435 layer_version = Layer_Version.objects.create(
291 layer=layer, 436 layer=layer,
@@ -293,7 +438,7 @@ class XhrLayer(View):
293 layer_source=LayerSource.TYPE_IMPORTED) 438 layer_source=LayerSource.TYPE_IMPORTED)
294 439
295 # Local layer 440 # Local layer
296 if ('local_source_dir' in layer_data) and layer.local_source_dir: 441 if ('local_source_dir' in layer_data): ### and layer.local_source_dir:
297 layer.local_source_dir = layer_data['local_source_dir'] 442 layer.local_source_dir = layer_data['local_source_dir']
298 # git layer 443 # git layer
299 elif 'vcs_url' in layer_data: 444 elif 'vcs_url' in layer_data:
@@ -325,6 +470,9 @@ class XhrLayer(View):
325 'layerdetailurl': 470 'layerdetailurl':
326 layer_dep.get_detailspage_url(project.pk)}) 471 layer_dep.get_detailspage_url(project.pk)})
327 472
473 # Scan the layer's content and update components
474 scan_layer_content(layer,layer_version)
475
328 except Layer_Version.DoesNotExist: 476 except Layer_Version.DoesNotExist:
329 return error_response("layer-dep-not-found") 477 return error_response("layer-dep-not-found")
330 except Project.DoesNotExist: 478 except Project.DoesNotExist:
@@ -1014,8 +1162,24 @@ class XhrProject(View):
1014 state=BuildRequest.REQ_INPROGRESS): 1162 state=BuildRequest.REQ_INPROGRESS):
1015 XhrBuildRequest.cancel_build(br) 1163 XhrBuildRequest.cancel_build(br)
1016 1164
1165 # gather potential orphaned local layers attached to this project
1166 project_local_layer_list = []
1167 for pl in ProjectLayer.objects.filter(project=project):
1168 if pl.layercommit.layer_source == LayerSource.TYPE_IMPORTED:
1169 project_local_layer_list.append(pl.layercommit.layer)
1170
1171 # deep delete the project and its dependencies
1017 project.delete() 1172 project.delete()
1018 1173
1174 # delete any local layers now orphaned
1175 _log("LAYER_ORPHAN_CHECK:Check for orphaned layers")
1176 for layer in project_local_layer_list:
1177 layer_refs = Layer_Version.objects.filter(layer=layer)
1178 _log("LAYER_ORPHAN_CHECK:Ref Count for '%s' = %d" % (layer.name,len(layer_refs)))
1179 if 0 == len(layer_refs):
1180 _log("LAYER_ORPHAN_CHECK:DELETE orpahned '%s'" % (layer.name))
1181 Layer.objects.filter(pk=layer.id).delete()
1182
1019 except Project.DoesNotExist: 1183 except Project.DoesNotExist:
1020 return error_response("Project %s does not exist" % 1184 return error_response("Project %s does not exist" %
1021 kwargs['project_id']) 1185 kwargs['project_id'])
diff --git a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
index 9f9eda1e1e..a5a6563d1a 100644
--- a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
+++ b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
@@ -67,6 +67,18 @@ function layerBtnsInit() {
67 }); 67 });
68 }); 68 });
69 69
70 $("td .set-default-recipe-btn").unbind('click');
71 $("td .set-default-recipe-btn").click(function(e){
72 e.preventDefault();
73 var recipe = $(this).data('recipe-name');
74
75 libtoaster.setDefaultImage(null, recipe,
76 function(){
77 /* Success */
78 window.location.replace(libtoaster.ctx.projectSpecificPageUrl);
79 });
80 });
81
70 82
71 $(".customise-btn").unbind('click'); 83 $(".customise-btn").unbind('click');
72 $(".customise-btn").click(function(e){ 84 $(".customise-btn").click(function(e){
diff --git a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
index 6f9b5d0f00..2e8863af26 100644
--- a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
+++ b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
@@ -465,6 +465,108 @@ var libtoaster = (function () {
465 $.cookie('toaster-notification', JSON.stringify(data), { path: '/'}); 465 $.cookie('toaster-notification', JSON.stringify(data), { path: '/'});
466 } 466 }
467 467
468 /* _updateProject:
469 * url: xhrProjectUpdateUrl or null for current project
470 * onsuccess: callback for successful execution
471 * onfail: callback for failed execution
472 */
473 function _updateProject (url, targets, default_image, onsuccess, onfail) {
474
475 if (!url)
476 url = libtoaster.ctx.xhrProjectUpdateUrl;
477
478 /* Flatten the array of targets into a space spearated list */
479 if (targets instanceof Array){
480 targets = targets.reduce(function(prevV, nextV){
481 return prev + ' ' + next;
482 });
483 }
484
485 $.ajax( {
486 type: "POST",
487 url: url,
488 data: { 'do_update' : 'True' , 'targets' : targets , 'default_image' : default_image , },
489 headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
490 success: function (_data) {
491 if (_data.error !== "ok") {
492 console.warn(_data.error);
493 } else {
494 if (onsuccess !== undefined) onsuccess(_data);
495 }
496 },
497 error: function (_data) {
498 console.warn("Call failed");
499 console.warn(_data);
500 if (onfail) onfail(data);
501 } });
502 }
503
504 /* _cancelProject:
505 * url: xhrProjectUpdateUrl or null for current project
506 * onsuccess: callback for successful execution
507 * onfail: callback for failed execution
508 */
509 function _cancelProject (url, onsuccess, onfail) {
510
511 if (!url)
512 url = libtoaster.ctx.xhrProjectCancelUrl;
513
514 $.ajax( {
515 type: "POST",
516 url: url,
517 data: { 'do_cancel' : 'True' },
518 headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
519 success: function (_data) {
520 if (_data.error !== "ok") {
521 console.warn(_data.error);
522 } else {
523 if (onsuccess !== undefined) onsuccess(_data);
524 }
525 },
526 error: function (_data) {
527 console.warn("Call failed");
528 console.warn(_data);
529 if (onfail) onfail(data);
530 } });
531 }
532
533 /* _setDefaultImage:
534 * url: xhrSetDefaultImageUrl or null for current project
535 * targets: an array or space separated list of targets to set as default
536 * onsuccess: callback for successful execution
537 * onfail: callback for failed execution
538 */
539 function _setDefaultImage (url, targets, onsuccess, onfail) {
540
541 if (!url)
542 url = libtoaster.ctx.xhrSetDefaultImageUrl;
543
544 /* Flatten the array of targets into a space spearated list */
545 if (targets instanceof Array){
546 targets = targets.reduce(function(prevV, nextV){
547 return prev + ' ' + next;
548 });
549 }
550
551 $.ajax( {
552 type: "POST",
553 url: url,
554 data: { 'targets' : targets },
555 headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
556 success: function (_data) {
557 if (_data.error !== "ok") {
558 console.warn(_data.error);
559 } else {
560 if (onsuccess !== undefined) onsuccess(_data);
561 }
562 },
563 error: function (_data) {
564 console.warn("Call failed");
565 console.warn(_data);
566 if (onfail) onfail(data);
567 } });
568 }
569
468 return { 570 return {
469 enableAjaxLoadingTimer: _enableAjaxLoadingTimer, 571 enableAjaxLoadingTimer: _enableAjaxLoadingTimer,
470 disableAjaxLoadingTimer: _disableAjaxLoadingTimer, 572 disableAjaxLoadingTimer: _disableAjaxLoadingTimer,
@@ -485,6 +587,9 @@ var libtoaster = (function () {
485 createCustomRecipe: _createCustomRecipe, 587 createCustomRecipe: _createCustomRecipe,
486 makeProjectNameValidation: _makeProjectNameValidation, 588 makeProjectNameValidation: _makeProjectNameValidation,
487 setNotification: _setNotification, 589 setNotification: _setNotification,
590 updateProject : _updateProject,
591 cancelProject : _cancelProject,
592 setDefaultImage : _setDefaultImage,
488 }; 593 };
489})(); 594})();
490 595
diff --git a/bitbake/lib/toaster/toastergui/static/js/mrbsection.js b/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
index c0c5fa9589..f07ccf8181 100644
--- a/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
+++ b/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
@@ -86,7 +86,7 @@ function mrbSectionInit(ctx){
86 if (buildFinished(build)) { 86 if (buildFinished(build)) {
87 // a build finished: reload the whole page so that the build 87 // a build finished: reload the whole page so that the build
88 // shows up in the builds table 88 // shows up in the builds table
89 window.location.reload(); 89 window.location.reload(true);
90 } 90 }
91 else if (stateChanged(build)) { 91 else if (stateChanged(build)) {
92 // update the whole template 92 // update the whole template
@@ -110,6 +110,8 @@ function mrbSectionInit(ctx){
110 // update the clone progress text 110 // update the clone progress text
111 selector = '#repos-cloned-percentage-' + build.id; 111 selector = '#repos-cloned-percentage-' + build.id;
112 $(selector).html(build.repos_cloned_percentage); 112 $(selector).html(build.repos_cloned_percentage);
113 selector = '#repos-cloned-progressitem-' + build.id;
114 $(selector).html('('+build.progress_item+')');
113 115
114 // update the recipe progress bar 116 // update the recipe progress bar
115 selector = '#repos-cloned-percentage-bar-' + build.id; 117 selector = '#repos-cloned-percentage-bar-' + build.id;
diff --git a/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js b/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
index 69220aaf57..3f9e186708 100644
--- a/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
+++ b/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
@@ -14,6 +14,9 @@ function projectTopBarInit(ctx) {
14 var newBuildTargetBuildBtn = $("#build-button"); 14 var newBuildTargetBuildBtn = $("#build-button");
15 var selectedTarget; 15 var selectedTarget;
16 16
17 var updateProjectBtn = $("#update-project-button");
18 var cancelProjectBtn = $("#cancel-project-button");
19
17 /* Project name change functionality */ 20 /* Project name change functionality */
18 projectNameFormToggle.click(function(e){ 21 projectNameFormToggle.click(function(e){
19 e.preventDefault(); 22 e.preventDefault();
@@ -89,6 +92,25 @@ function projectTopBarInit(ctx) {
89 }, null); 92 }, null);
90 }); 93 });
91 94
95 updateProjectBtn.click(function (e) {
96 e.preventDefault();
97
98 selectedTarget = { name: "_PROJECT_PREPARE_" };
99
100 /* Save current default build image, fire off the build */
101 libtoaster.updateProject(null, selectedTarget.name, newBuildTargetInput.val().trim(),
102 function(){
103 window.location.replace(libtoaster.ctx.projectSpecificPageUrl);
104 }, null);
105 });
106
107 cancelProjectBtn.click(function (e) {
108 e.preventDefault();
109
110 /* redirect to 'done/canceled' landing page */
111 window.location.replace(libtoaster.ctx.landingSpecificCancelURL);
112 });
113
92 /* Call makeProjectNameValidation function */ 114 /* Call makeProjectNameValidation function */
93 libtoaster.makeProjectNameValidation($("#project-name-change-input"), 115 libtoaster.makeProjectNameValidation($("#project-name-change-input"),
94 $("#hint-error-project-name"), $("#validate-project-name"), 116 $("#hint-error-project-name"), $("#validate-project-name"),
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index dca2fa2913..03bd2ae9c6 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -35,6 +35,8 @@ from toastergui.tablefilter import TableFilterActionToggle
35from toastergui.tablefilter import TableFilterActionDateRange 35from toastergui.tablefilter import TableFilterActionDateRange
36from toastergui.tablefilter import TableFilterActionDay 36from toastergui.tablefilter import TableFilterActionDay
37 37
38import os
39
38class ProjectFilters(object): 40class ProjectFilters(object):
39 @staticmethod 41 @staticmethod
40 def in_project(project_layers): 42 def in_project(project_layers):
@@ -339,6 +341,8 @@ class RecipesTable(ToasterTable):
339 'filter_name' : "in_current_project", 341 'filter_name' : "in_current_project",
340 'static_data_name' : "add-del-layers", 342 'static_data_name' : "add-del-layers",
341 'static_data_template' : '{% include "recipe_btn.html" %}'} 343 'static_data_template' : '{% include "recipe_btn.html" %}'}
344 if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
345 build_col['static_data_template'] = '{% include "recipe_add_btn.html" %}'
342 346
343 def get_context_data(self, **kwargs): 347 def get_context_data(self, **kwargs):
344 project = Project.objects.get(pk=kwargs['pid']) 348 project = Project.objects.get(pk=kwargs['pid'])
diff --git a/bitbake/lib/toaster/toastergui/templates/base_specific.html b/bitbake/lib/toaster/toastergui/templates/base_specific.html
new file mode 100644
index 0000000000..e377cadd73
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/base_specific.html
@@ -0,0 +1,128 @@
1<!DOCTYPE html>
2{% load static %}
3{% load projecttags %}
4{% load project_url_tag %}
5<html lang="en">
6 <head>
7 <title>
8 {% block title %} Toaster {% endblock %}
9 </title>
10 <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" type="text/css"/>
11 <!--link rel="stylesheet" href="{% static 'css/bootstrap-theme.css' %}" type="text/css"/-->
12 <link rel="stylesheet" href="{% static 'css/font-awesome.min.css' %}" type='text/css'/>
13 <link rel="stylesheet" href="{% static 'css/default.css' %}" type='text/css'/>
14
15 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
16 <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
17 <script src="{% static 'js/jquery-2.0.3.min.js' %}">
18 </script>
19 <script src="{% static 'js/jquery.cookie.js' %}">
20 </script>
21 <script src="{% static 'js/bootstrap.min.js' %}">
22 </script>
23 <script src="{% static 'js/typeahead.jquery.js' %}">
24 </script>
25 <script src="{% static 'js/jsrender.min.js' %}">
26 </script>
27 <script src="{% static 'js/highlight.pack.js' %}">
28 </script>
29 <script src="{% static 'js/libtoaster.js' %}">
30 </script>
31 {% if DEBUG %}
32 <script>
33 libtoaster.debug = true;
34 </script>
35 {% endif %}
36 <script>
37 /* Set JsRender delimiters (mrb_section.html) different than Django's */
38 $.views.settings.delimiters("<%", "%>");
39
40 /* This table allows Django substitutions to be passed to libtoaster.js */
41 libtoaster.ctx = {
42 jsUrl : "{% static 'js/' %}",
43 htmlUrl : "{% static 'html/' %}",
44 projectsUrl : "{% url 'all-projects' %}",
45 projectsTypeAheadUrl: {% url 'xhr_projectstypeahead' as prjurl%}{{prjurl|json}},
46 {% if project.id %}
47 landingSpecificURL : "{% url 'landing_specific' project.id %}",
48 landingSpecificCancelURL : "{% url 'landing_specific_cancel' project.id %}",
49 projectId : {{project.id}},
50 projectPageUrl : {% url 'project' project.id as purl %}{{purl|json}},
51 projectSpecificPageUrl : {% url 'project_specific' project.id as purl %}{{purl|json}},
52 xhrProjectUrl : {% url 'xhr_project' project.id as pxurl %}{{pxurl|json}},
53 projectName : {{project.name|json}},
54 recipesTypeAheadUrl: {% url 'xhr_recipestypeahead' project.id as paturl%}{{paturl|json}},
55 layersTypeAheadUrl: {% url 'xhr_layerstypeahead' project.id as paturl%}{{paturl|json}},
56 machinesTypeAheadUrl: {% url 'xhr_machinestypeahead' project.id as paturl%}{{paturl|json}},
57 distrosTypeAheadUrl: {% url 'xhr_distrostypeahead' project.id as paturl%}{{paturl|json}},
58 projectBuildsUrl: {% url 'projectbuilds' project.id as pburl %}{{pburl|json}},
59 xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}",
60 projectId : {{project.id}},
61 xhrBuildRequestUrl: "{% url 'xhr_buildrequest' project.id %}",
62 mostRecentBuildsUrl: "{% url 'most_recent_builds' %}?project_id={{project.id}}",
63 xhrProjectUpdateUrl: "{% url 'xhr_projectupdate' project.id %}",
64 xhrProjectCancelUrl: "{% url 'landing_specific_cancel' project.id %}",
65 xhrSetDefaultImageUrl: "{% url 'xhr_setdefaultimage' project.id %}",
66 {% else %}
67 mostRecentBuildsUrl: "{% url 'most_recent_builds' %}",
68 projectId : undefined,
69 projectPageUrl : undefined,
70 projectName : undefined,
71 {% endif %}
72 };
73 </script>
74 {% block extraheadcontent %}
75 {% endblock %}
76 </head>
77
78 <body>
79
80 {% csrf_token %}
81 <div id="loading-notification" class="alert alert-warning lead text-center" style="display:none">
82 Loading <i class="fa-pulse icon-spinner"></i>
83 </div>
84
85 <div id="change-notification" class="alert alert-info alert-dismissible change-notification" style="display:none">
86 <button type="button" class="close" id="hide-alert" data-toggle="alert">&times;</button>
87 <span id="change-notification-msg"></span>
88 </div>
89
90 <nav class="navbar navbar-default navbar-fixed-top">
91 <div class="container-fluid">
92 <div class="navbar-header">
93 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#global-nav" aria-expanded="false">
94 <span class="sr-only">Toggle navigation</span>
95 <span class="icon-bar"></span>
96 <span class="icon-bar"></span>
97 <span class="icon-bar"></span>
98 </button>
99 <div class="toaster-navbar-brand">
100 {% if project_specific %}
101 <img class="logo" src="{% static 'img/logo.png' %}" class="" alt="Yocto Project logo"/>
102 Toaster
103 {% else %}
104 <a href="/">
105 </a>
106 <a href="/">
107 <img class="logo" src="{% static 'img/logo.png' %}" class="" alt="Yocto Project logo"/>
108 </a>
109 <a class="brand" href="/">Toaster</a>
110 {% endif %}
111 {% if DEBUG %}
112 <span class="glyphicon glyphicon-info-sign" title="<strong>Toaster version information</strong>" data-content="<dl><dt>Git branch</dt><dd>{{TOASTER_BRANCH}}</dd><dt>Git revision</dt><dd>{{TOASTER_REVISION}}</dd></dl>"></i>
113 {% endif %}
114 </div>
115 </div>
116 <div class="collapse navbar-collapse" id="global-nav">
117 <ul class="nav navbar-nav">
118 <h3> Project Configuration Page </h3>
119 </div>
120 </div>
121 </nav>
122
123 <div class="container-fluid">
124 {% block pagecontent %}
125 {% endblock %}
126 </div>
127 </body>
128</html>
diff --git a/bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html b/bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html
new file mode 100644
index 0000000000..d0b588de98
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html
@@ -0,0 +1,48 @@
1{% extends "base_specific.html" %}
2
3{% load projecttags %}
4{% load humanize %}
5
6{% block title %} {{title}} - {{project.name}} - Toaster {% endblock %}
7
8{% block pagecontent %}
9
10<div class="row">
11 {% include "project_specific_topbar.html" %}
12 <script type="text/javascript">
13$(document).ready(function(){
14 $("#config-nav .nav li a").each(function(){
15 if (window.location.pathname === $(this).attr('href'))
16 $(this).parent().addClass('active');
17 else
18 $(this).parent().removeClass('active');
19 });
20
21 $("#topbar-configuration-tab").addClass("active")
22 });
23 </script>
24
25 <!-- only on config pages -->
26 <div id="config-nav" class="col-md-2">
27 <ul class="nav nav-pills nav-stacked">
28 <li><a class="nav-parent" href="{% url 'project' project.id %}">Configuration</a></li>
29 <li class="nav-header">Compatible metadata</li>
30 <li><a href="{% url 'projectcustomimages' project.id %}">Custom images</a></li>
31 <li><a href="{% url 'projectimagerecipes' project.id %}">Image recipes</a></li>
32 <li><a href="{% url 'projectsoftwarerecipes' project.id %}">Software recipes</a></li>
33 <li><a href="{% url 'projectmachines' project.id %}">Machines</a></li>
34 <li><a href="{% url 'projectlayers' project.id %}">Layers</a></li>
35 <li><a href="{% url 'projectdistros' project.id %}">Distros</a></li>
36 <li class="nav-header">Extra configuration</li>
37 <li><a href="{% url 'projectconf' project.id %}">BitBake variables</a></li>
38
39 <li class="nav-header">Actions</li>
40 </ul>
41 </div>
42 <div class="col-md-10">
43 {% block projectinfomain %}{% endblock %}
44 </div>
45
46</div>
47{% endblock %}
48
diff --git a/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html b/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html
index b3eabe1a26..99fbb38970 100644
--- a/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html
+++ b/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html
@@ -1,4 +1,4 @@
1{% extends "baseprojectpage.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %}
2{% load projecttags %} 2{% load projecttags %}
3{% load humanize %} 3{% load humanize %}
4{% load static %} 4{% load static %}
diff --git a/bitbake/lib/toaster/toastergui/templates/importlayer.html b/bitbake/lib/toaster/toastergui/templates/importlayer.html
index 97d52c76c1..e0c987eef1 100644
--- a/bitbake/lib/toaster/toastergui/templates/importlayer.html
+++ b/bitbake/lib/toaster/toastergui/templates/importlayer.html
@@ -1,4 +1,4 @@
1{% extends "base.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %}
2{% load projecttags %} 2{% load projecttags %}
3{% load humanize %} 3{% load humanize %}
4{% load static %} 4{% load static %}
@@ -6,7 +6,7 @@
6{% block pagecontent %} 6{% block pagecontent %}
7 7
8<div class="row"> 8<div class="row">
9 {% include "projecttopbar.html" %} 9 {% include project_specific|yesno:"project_specific_topbar.html,projecttopbar.html" %}
10 {% if project and project.release %} 10 {% if project and project.release %}
11 <script src="{% static 'js/layerDepsModal.js' %}"></script> 11 <script src="{% static 'js/layerDepsModal.js' %}"></script>
12 <script src="{% static 'js/importlayer.js' %}"></script> 12 <script src="{% static 'js/importlayer.js' %}"></script>
diff --git a/bitbake/lib/toaster/toastergui/templates/landing_specific.html b/bitbake/lib/toaster/toastergui/templates/landing_specific.html
new file mode 100644
index 0000000000..e289c7d4a5
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/landing_specific.html
@@ -0,0 +1,50 @@
1{% extends "base_specific.html" %}
2
3{% load static %}
4{% load projecttags %}
5{% load humanize %}
6
7{% block title %} Welcome to Toaster {% endblock %}
8
9{% block pagecontent %}
10
11 <div class="container">
12 <div class="row">
13 <!-- Empty - no build module -->
14 <div class="page-header top-air">
15 <h1>
16 Configuration {% if status == "cancel" %}Canceled{% else %}Completed{% endif %}! You can now close this window.
17 </h1>
18 </div>
19 <div class="alert alert-info lead">
20 <p>
21 Your project configuration {% if status == "cancel" %}changes have been canceled{% else %}has completed!{% endif %}
22 <br>
23 <br>
24 <ul>
25 <li>
26 The Toaster instance for project configuration has been shut down
27 </li>
28 <li>
29 You can start Toaster independently for advanced project management and analysis:
30 <pre><code>
31 Set up bitbake environment:
32 $ cd {{install_dir}}
33 $ . oe-init-build-env [toaster_server]
34
35 Option 1: Start a local Toaster server, open local browser to "localhost:8000"
36 $ . toaster start webport=8000
37
38 Option 2: Start a shared Toaster server, open any browser to "[host_ip]:8000"
39 $ . toaster start webport=0.0.0.0:8000
40
41 To stop the Toaster server:
42 $ . toaster stop
43 </code></pre>
44 </li>
45 </ul>
46 </p>
47 </div>
48 </div>
49
50{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/layerdetails.html b/bitbake/lib/toaster/toastergui/templates/layerdetails.html
index e0069db80c..1e26e31c8b 100644
--- a/bitbake/lib/toaster/toastergui/templates/layerdetails.html
+++ b/bitbake/lib/toaster/toastergui/templates/layerdetails.html
@@ -1,4 +1,4 @@
1{% extends "base.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %}
2{% load projecttags %} 2{% load projecttags %}
3{% load humanize %} 3{% load humanize %}
4{% load static %} 4{% load static %}
@@ -310,6 +310,7 @@
310 {% endwith %} 310 {% endwith %}
311 {% endwith %} 311 {% endwith %}
312 </div> 312 </div>
313
313 </div> <!-- end tab content --> 314 </div> <!-- end tab content -->
314 </div> <!-- end tabable --> 315 </div> <!-- end tabable -->
315 316
diff --git a/bitbake/lib/toaster/toastergui/templates/mrb_section.html b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
index c5b9fe90d3..98d9fac822 100644
--- a/bitbake/lib/toaster/toastergui/templates/mrb_section.html
+++ b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
@@ -119,7 +119,7 @@
119 title="Toaster is cloning the repos required for your build"> 119 title="Toaster is cloning the repos required for your build">
120 </span> 120 </span>
121 121
122 Cloning <span id="repos-cloned-percentage-<%:id%>"><%:repos_cloned_percentage%></span>% complete 122 Cloning <span id="repos-cloned-percentage-<%:id%>"><%:repos_cloned_percentage%></span>% complete <span id="repos-cloned-progressitem-<%:id%>">(<%:progress_item%>)</span>
123 123
124 <%include tmpl='#cancel-template'/%> 124 <%include tmpl='#cancel-template'/%>
125 </div> 125 </div>
diff --git a/bitbake/lib/toaster/toastergui/templates/newcustomimage.html b/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
index 980179a406..0766e5e4cf 100644
--- a/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
+++ b/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
@@ -1,4 +1,4 @@
1{% extends "base.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %}
2{% load projecttags %} 2{% load projecttags %}
3{% load humanize %} 3{% load humanize %}
4{% load static %} 4{% load static %}
@@ -8,7 +8,7 @@
8 8
9<div class="row"> 9<div class="row">
10 10
11 {% include "projecttopbar.html" %} 11 {% include project_specific|yesno:"project_specific_topbar.html,projecttopbar.html" %}
12 12
13 <div class="col-md-12"> 13 <div class="col-md-12">
14 {% url table_name project.id as xhr_table_url %} 14 {% url table_name project.id as xhr_table_url %}
diff --git a/bitbake/lib/toaster/toastergui/templates/newproject_specific.html b/bitbake/lib/toaster/toastergui/templates/newproject_specific.html
new file mode 100644
index 0000000000..cfa77f2e40
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/newproject_specific.html
@@ -0,0 +1,95 @@
1{% extends "base.html" %}
2{% load projecttags %}
3{% load humanize %}
4
5{% block title %} Create a new project - Toaster {% endblock %}
6
7{% block pagecontent %}
8<div class="row">
9 <div class="col-md-12">
10 <div class="page-header">
11 <h1>Create a new project</h1>
12 </div>
13 {% if alert %}
14 <div class="alert alert-danger" role="alert">{{alert}}</div>
15 {% endif %}
16
17 <form method="POST" action="{%url "newproject_specific" project_pk %}">{% csrf_token %}
18 <div class="form-group" id="validate-project-name">
19 <label class="control-label">Project name <span class="text-muted">(required)</span></label>
20 <input type="text" class="form-control" required id="new-project-name" name="display_projectname" value="{{projectname}}" disabled>
21 </div>
22 <p class="help-block text-danger" style="display: none;" id="hint-error-project-name">A project with this name exists. Project names must be unique.</p>
23 <input type="hidden" name="ptype" value="build" />
24 <input type="hidden" name="projectname" value="{{projectname}}" />
25
26 {% if releases.count > 0 %}
27 <div class="release form-group">
28 {% if releases.count > 1 %}
29 <label class="control-label">
30 Release
31 <span class="glyphicon glyphicon-question-sign get-help" title="The version of the build system you want to use"></span>
32 </label>
33 <select name="projectversion" id="projectversion" class="form-control">
34 {% for release in releases %}
35 <option value="{{release.id}}"
36 {%if defaultbranch == release.name %}
37 selected
38 {%endif%}
39 >{{release.description}}</option>
40 {% endfor %}
41 </select>
42 <div class="row">
43 <div class="col-md-4">
44 {% for release in releases %}
45 <div class="helptext" id="description-{{release.id}}" style="display: none">
46 <span class="help-block">{{release.helptext|safe}}</span>
47 </div>
48 {% endfor %}
49 {% else %}
50 <input type="hidden" name="projectversion" value="{{releases.0.id}}"/>
51 {% endif %}
52 </div>
53 </div>
54 </fieldset>
55 {% endif %}
56 <div class="top-air">
57 <input type="submit" id="create-project-button" class="btn btn-primary btn-lg" value="Create project"/>
58 <span class="help-inline" style="vertical-align:middle;">To create a project, you need to specify the release</span>
59 </div>
60
61 </form>
62 </div>
63 </div>
64
65 <script type="text/javascript">
66 $(document).ready(function () {
67 // hide the new project button, name is preset
68 $("#new-project-button").hide();
69
70 // enable submit button when all required fields are populated
71 $("input#new-project-name").on('input', function() {
72 if ($("input#new-project-name").val().length > 0 ){
73 $('.btn-primary').removeAttr('disabled');
74 $(".help-inline").css('visibility','hidden');
75 }
76 else {
77 $('.btn-primary').attr('disabled', 'disabled');
78 $(".help-inline").css('visibility','visible');
79 }
80 });
81
82 // show relevant help text for the selected release
83 var selected_release = $('select').val();
84 $("#description-" + selected_release).show();
85
86 $('select').change(function(){
87 var new_release = $('select').val();
88 $(".helptext").hide();
89 $('#description-' + new_release).fadeIn();
90 });
91
92 });
93 </script>
94
95{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/project.html b/bitbake/lib/toaster/toastergui/templates/project.html
index 11603d1e12..fa41e3c909 100644
--- a/bitbake/lib/toaster/toastergui/templates/project.html
+++ b/bitbake/lib/toaster/toastergui/templates/project.html
@@ -1,4 +1,4 @@
1{% extends "baseprojectpage.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %}
2 2
3{% load projecttags %} 3{% load projecttags %}
4{% load humanize %} 4{% load humanize %}
@@ -18,7 +18,7 @@
18 try { 18 try {
19 projectPageInit(ctx); 19 projectPageInit(ctx);
20 } catch (e) { 20 } catch (e) {
21 document.write("Sorry, An error has occurred loading this page"); 21 document.write("Sorry, An error has occurred loading this page (project):"+e);
22 console.warn(e); 22 console.warn(e);
23 } 23 }
24 }); 24 });
@@ -93,6 +93,7 @@
93 </form> 93 </form>
94 </div> 94 </div>
95 95
96 {% if not project_specific %}
96 <div class="well well-transparent"> 97 <div class="well well-transparent">
97 <h3>Most built recipes</h3> 98 <h3>Most built recipes</h3>
98 99
@@ -105,6 +106,7 @@
105 </ul> 106 </ul>
106 <button class="btn btn-primary" id="freq-build-btn" disabled="disabled">Build selected recipes</button> 107 <button class="btn btn-primary" id="freq-build-btn" disabled="disabled">Build selected recipes</button>
107 </div> 108 </div>
109 {% endif %}
108 110
109 <div class="well well-transparent"> 111 <div class="well well-transparent">
110 <h3>Project release</h3> 112 <h3>Project release</h3>
@@ -157,5 +159,6 @@
157 <ul class="list-unstyled lead" id="layers-in-project-list"> 159 <ul class="list-unstyled lead" id="layers-in-project-list">
158 </ul> 160 </ul>
159 </div> 161 </div>
162
160</div> 163</div>
161{% endblock %} 164{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/project_specific.html b/bitbake/lib/toaster/toastergui/templates/project_specific.html
new file mode 100644
index 0000000000..f625d18baf
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/project_specific.html
@@ -0,0 +1,162 @@
1{% extends "baseprojectspecificpage.html" %}
2
3{% load projecttags %}
4{% load humanize %}
5{% load static %}
6
7{% block title %} Configuration - {{project.name}} - Toaster {% endblock %}
8{% block projectinfomain %}
9
10<script src="{% static 'js/layerDepsModal.js' %}"></script>
11<script src="{% static 'js/projectpage.js' %}"></script>
12<script>
13 $(document).ready(function (){
14 var ctx = {
15 testReleaseChangeUrl: "{% url 'xhr_testreleasechange' project.id %}",
16 };
17
18 try {
19 projectPageInit(ctx);
20 } catch (e) {
21 document.write("Sorry, An error has occurred loading this page");
22 console.warn(e);
23 }
24 });
25</script>
26
27<div id="delete-project-modal" class="modal fade" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
28 <div class="modal-dialog">
29 <div class="modal-content">
30 <div class="modal-header">
31 <h4>Are you sure you want to delete this project?</h4>
32 </div>
33 <div class="modal-body">
34 <p>Deleting the <strong class="project-name"></strong> project
35 will:</p>
36 <ul>
37 <li>Cancel its builds currently in progress</li>
38 <li>Remove its configuration information</li>
39 <li>Remove its imported layers</li>
40 <li>Remove its custom images</li>
41 <li>Remove all its build information</li>
42 </ul>
43 </div>
44 <div class="modal-footer">
45 <button type="button" class="btn btn-primary" id="delete-project-confirmed">
46 <span data-role="submit-state">Delete project</span>
47 <span data-role="loading-state" style="display:none">
48 <span class="fa-pulse">
49 <i class="fa-pulse icon-spinner"></i>
50 </span>
51 &nbsp;Deleting project...
52 </span>
53 </button>
54 <button type="button" class="btn btn-link" data-dismiss="modal">Cancel</button>
55 </div>
56 </div><!-- /.modal-content -->
57 </div><!-- /.modal-dialog -->
58</div>
59
60
61<div class="row" id="project-page" style="display:none">
62 <div class="col-md-6">
63 <div class="well well-transparent" id="machine-section">
64 <h3>Machine</h3>
65
66 <p class="lead"><span id="project-machine-name"></span> <span class="glyphicon glyphicon-edit" id="change-machine-toggle"></span></p>
67
68 <form id="select-machine-form" style="display:none;" class="form-inline">
69 <span class="help-block">Machine suggestions come from the list of layers added to your project. If you don't see the machine you are looking for, <a href="{% url 'projectmachines' project.id %}">check the full list of machines</a></span>
70 <div class="form-group" id="machine-input-form">
71 <input class="form-control" id="machine-change-input" autocomplete="off" value="" data-provide="typeahead" data-minlength="1" data-autocomplete="off" type="text">
72 </div>
73 <button id="machine-change-btn" class="btn btn-default" type="button">Save</button>
74 <a href="#" id="cancel-machine-change" class="btn btn-link">Cancel</a>
75 <span class="help-block text-danger" id="invalid-machine-name-help" style="display:none">A valid machine name cannot include spaces.</span>
76 <p class="form-link"><a href="{% url 'projectmachines' project.id %}">View compatible machines</a></p>
77 </form>
78 </div>
79
80 <div class="well well-transparent" id="distro-section">
81 <h3>Distro</h3>
82
83 <p class="lead"><span id="project-distro-name"></span> <span class="glyphicon glyphicon-edit" id="change-distro-toggle"></span></p>
84
85 <form id="select-distro-form" style="display:none;" class="form-inline">
86 <span class="help-block">Distro suggestions come from the Layer Index</a></span>
87 <div class="form-group">
88 <input class="form-control" id="distro-change-input" autocomplete="off" value="" data-provide="typeahead" data-minlength="1" data-autocomplete="off" type="text">
89 </div>
90 <button id="distro-change-btn" class="btn btn-default" type="button">Save</button>
91 <a href="#" id="cancel-distro-change" class="btn btn-link">Cancel</a>
92 <p class="form-link"><a href="{% url 'projectdistros' project.id %}">View compatible distros</a></p>
93 </form>
94 </div>
95
96 <div class="well well-transparent">
97 <h3>Most built recipes</h3>
98
99 <div class="alert alert-info" style="display:none" id="no-most-built">
100 <h4>You haven't built any recipes yet</h4>
101 <p class="form-link"><a href="{% url 'projectimagerecipes' project.id %}">Choose a recipe to build</a></p>
102 </div>
103
104 <ul class="list-unstyled lead" id="freq-build-list">
105 </ul>
106 <button class="btn btn-primary" id="freq-build-btn" disabled="disabled">Build selected recipes</button>
107 </div>
108
109 <div class="well well-transparent">
110 <h3>Project release</h3>
111
112 <p class="lead"><span id="project-release-title"></span>
113
114 <!-- Comment out the ability to change the project release, until we decide what to do with this functionality -->
115
116 <!--i title="" data-original-title="" id="release-change-toggle" class="icon-pencil"></i-->
117 </p>
118
119 <!-- Comment out the ability to change the project release, until we decide what to do with this functionality -->
120
121 <!--form class="form-inline" id="change-release-form" style="display:none;">
122 <select></select>
123 <button class="btn" style="margin-left:5px;" id="change-release-btn">Change</button> <a href="#" id="cancel-release-change" class="btn btn-link">Cancel</a>
124 </form-->
125 </div>
126 </div>
127
128 <div class="col-md-6">
129 <div class="well well-transparent" id="layer-container">
130 <h3>Layers <span class="counter">(<span id="project-layers-count"></span>)</span>
131 <span title="OpenEmbedded organises recipes and machines into thematic groups called <strong>layers</strong>. Click on a layer name to see the recipes and machines it includes." class="glyphicon glyphicon-question-sign get-help"></span>
132 </h3>
133
134 <div class="alert alert-warning" id="no-layers-in-project" style="display:none">
135 <h4>This project has no layers</h4>
136 In order to build this project you need to add some layers first. For that you can:
137 <ul>
138 <li><a href="{% url 'projectlayers' project.id %}">Choose from the layers compatible with this project</a></li>
139 <li><a href="{% url 'importlayer' project.id %}">Import a layer</a></li>
140 <li><a href="http://www.yoctoproject.org/docs/current/dev-manual/dev-manual.html#understanding-and-creating-layers" target="_blank">Read about layers in the documentation</a></li>
141 <li>Or type a layer name below</li>
142 </ul>
143 </div>
144
145 <form class="form-inline">
146 <div class="form-group">
147 <input id="layer-add-input" class="form-control" autocomplete="off" placeholder="Type a layer name" data-minlength="1" data-autocomplete="off" data-provide="typeahead" data-source="" type="text">
148 </div>
149 <button id="add-layer-btn" class="btn btn-default" disabled>Add layer</button>
150 <p class="form-link">
151 <a href="{% url 'projectlayers' project.id %}" id="view-compatible-layers">View compatible layers</a>
152 <span class="text-muted">|</span>
153 <a href="{% url 'importlayer' project.id %}">Import layer</a>
154 </p>
155 </form>
156
157 <ul class="list-unstyled lead" id="layers-in-project-list">
158 </ul>
159 </div>
160
161</div>
162{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html b/bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html
new file mode 100644
index 0000000000..622787c4bc
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html
@@ -0,0 +1,80 @@
1{% load static %}
2<script src="{% static 'js/projecttopbar.js' %}"></script>
3<script>
4 $(document).ready(function () {
5 var ctx = {
6 numProjectLayers : {{project.get_project_layer_versions.count}},
7 machine : "{{project.get_current_machine_name|default_if_none:""}}",
8 }
9
10 try {
11 projectTopBarInit(ctx);
12 } catch (e) {
13 document.write("Sorry, An error has occurred loading this page (pstb):"+e);
14 console.warn(e);
15 }
16 });
17</script>
18
19<div class="col-md-12">
20 <div class="alert alert-success alert-dismissible change-notification" id="project-created-notification" style="display:none">
21 <button type="button" class="close" data-dismiss="alert">&times;</button>
22 <p>Your project <strong>{{project.name}}</strong> has been created. You can now <a class="alert-link" href="{% url 'projectmachines' project.id %}">select your target machine</a> and <a class="alert-link" href="{% url 'projectimagerecipes' project.id %}">choose image recipes</a> to build.</p>
23 </div>
24 <!-- project name -->
25 <div class="page-header">
26 <h1 id="project-name-container">
27 <span class="project-name">{{project.name}}</span>
28 {% if project.is_default %}
29 <span class="glyphicon glyphicon-question-sign get-help" title="This project shows information about the builds you start from the command line while Toaster is running"></span>
30 {% endif %}
31 </h1>
32 <form id="project-name-change-form" class="form-inline" style="display: none;">
33 <div class="form-group">
34 <input class="form-control input-lg" type="text" id="project-name-change-input" autocomplete="off" value="{{project.name}}">
35 </div>
36 <button id="project-name-change-btn" class="btn btn-default btn-lg" type="button">Save</button>
37 <a href="#" id="project-name-change-cancel" class="btn btn-lg btn-link">Cancel</a>
38 </form>
39 </div>
40
41 {% with mrb_type='project' %}
42 {% include "mrb_section.html" %}
43 {% endwith %}
44
45 {% if not project.is_default %}
46 <div id="project-topbar">
47 <ul class="nav nav-tabs">
48 <li id="topbar-configuration-tab">
49 <a href="{% url 'project_specific' project.id %}">
50 Configuration
51 </a>
52 </li>
53 <li>
54 <a href="{% url 'importlayer' project.id %}">
55 Import layer
56 </a>
57 </li>
58 <li>
59 <a href="{% url 'newcustomimage' project.id %}">
60 New custom image
61 </a>
62 </li>
63 <li class="pull-right">
64 <form class="form-inline">
65 <div class="form-group">
66 <span class="glyphicon glyphicon-question-sign get-help" data-placement="left" title="Type the name of one or more recipes you want to build, separated by a space. You can also specify a task by appending a colon and a task name to the recipe name, like so: <code>busybox:clean</code>"></span>
67 <input id="build-input" type="text" class="form-control input-lg" placeholder="Select the default image recipe" autocomplete="off" disabled value="{{project.get_default_image}}">
68 </div>
69 {% if project.get_is_new %}
70 <button id="update-project-button" class="btn btn-primary btn-lg" data-project-id="{{project.id}}">Prepare Project</button>
71 {% else %}
72 <button id="cancel-project-button" class="btn info btn-lg" data-project-id="{{project.id}}">Cancel</button>
73 <button id="update-project-button" class="btn btn-primary btn-lg" data-project-id="{{project.id}}">Update</button>
74 {% endif %}
75 </form>
76 </li>
77 </ul>
78 </div>
79 {% endif %}
80</div>
diff --git a/bitbake/lib/toaster/toastergui/templates/projectconf.html b/bitbake/lib/toaster/toastergui/templates/projectconf.html
index 933c588f34..fb20b26f22 100644
--- a/bitbake/lib/toaster/toastergui/templates/projectconf.html
+++ b/bitbake/lib/toaster/toastergui/templates/projectconf.html
@@ -1,4 +1,4 @@
1{% extends "baseprojectpage.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %}
2{% load projecttags %} 2{% load projecttags %}
3{% load humanize %} 3{% load humanize %}
4 4
@@ -438,8 +438,11 @@ function onEditPageUpdate(data) {
438 var_context='m'; 438 var_context='m';
439 } 439 }
440 } 440 }
441 if (configvars_sorted[i][0].startsWith("INTERNAL_")) {
442 var_context='m';
443 }
441 if (var_context == undefined) { 444 if (var_context == undefined) {
442 orightml += '<dt><span id="config_var_entry_'+configvars_sorted[i][2]+'" class="js-config-var-name"></span><span class="glyphicon glyphicon-trash js-icon-trash-config_var" id="config_var_trash_'+configvars_sorted[i][2]+'" x-data="'+configvars_sorted[i][2]+'"></span> </dt>' 445 orightml += '<dt><span id="config_var_entry_'+configvars_sorted[i][2]+'" class="js-config-var-name"></span><span class="glyphicon glyphicon-trash js-icon-trash-config_var" id="config_var_trash_'+configvars_sorted[i][2]+'" x-data="'+configvars_sorted[i][2]+'"></span> </dt>'
443 orightml += '<dd class="variable-list">' 446 orightml += '<dd class="variable-list">'
444 orightml += ' <span class="lead" id="config_var_value_'+configvars_sorted[i][2]+'"></span>' 447 orightml += ' <span class="lead" id="config_var_value_'+configvars_sorted[i][2]+'"></span>'
445 orightml += ' <span class="glyphicon glyphicon-edit js-icon-pencil-config_var" x-data="'+configvars_sorted[i][2]+'"></span>' 448 orightml += ' <span class="glyphicon glyphicon-edit js-icon-pencil-config_var" x-data="'+configvars_sorted[i][2]+'"></span>'
diff --git a/bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html b/bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html
new file mode 100644
index 0000000000..06c464561e
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html
@@ -0,0 +1,23 @@
1<a data-recipe-name="{{data.name}}" class="btn btn-default btn-block layer-exists-{{data.layer_version.pk}} set-default-recipe-btn" style="margin-top: 5px;
2 {% if data.layer_version.pk not in extra.current_layers %}
3 display:none;
4 {% endif %}"
5 >
6 Set recipe
7</a>
8<a class="btn btn-default btn-block layerbtn layer-add-{{data.layer_version.pk}}"
9 data-layer='{
10 "id": {{data.layer_version.pk}},
11 "name": "{{data.layer_version.layer.name}}",
12 "layerdetailurl": "{%url "layerdetails" extra.pid data.layer_version.pk%}",
13 "xhrLayerUrl": "{% url "xhr_layer" extra.pid data.layer_version.pk %}"
14 }' data-directive="add"
15 {% if data.layer_version.pk in extra.current_layers %}
16 style="display:none;"
17 {% endif %}
18>
19 <span class="glyphicon glyphicon-plus"></span>
20 Add layer
21 <span class="glyphicon glyphicon-question-sign get-help" title="To set this
22 recipe you must first add the {{data.layer_version.layer.name}} layer to your project"></i>
23</a>
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index e07b0efc1f..dc03e30356 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -116,6 +116,11 @@ urlpatterns = [
116 tables.ProjectBuildsTable.as_view(template_name="projectbuilds-toastertable.html"), 116 tables.ProjectBuildsTable.as_view(template_name="projectbuilds-toastertable.html"),
117 name='projectbuilds'), 117 name='projectbuilds'),
118 118
119 url(r'^newproject_specific/(?P<pid>\d+)/$', views.newproject_specific, name='newproject_specific'),
120 url(r'^project_specific/(?P<pid>\d+)/$', views.project_specific, name='project_specific'),
121 url(r'^landing_specific/(?P<pid>\d+)/$', views.landing_specific, name='landing_specific'),
122 url(r'^landing_specific_cancel/(?P<pid>\d+)/$', views.landing_specific_cancel, name='landing_specific_cancel'),
123
119 # the import layer is a project-specific functionality; 124 # the import layer is a project-specific functionality;
120 url(r'^project/(?P<pid>\d+)/importlayer$', views.importlayer, name='importlayer'), 125 url(r'^project/(?P<pid>\d+)/importlayer$', views.importlayer, name='importlayer'),
121 126
@@ -233,6 +238,14 @@ urlpatterns = [
233 api.XhrBuildRequest.as_view(), 238 api.XhrBuildRequest.as_view(),
234 name='xhr_buildrequest'), 239 name='xhr_buildrequest'),
235 240
241 url(r'^xhr_projectupdate/project/(?P<pid>\d+)$',
242 api.XhrProjectUpdate.as_view(),
243 name='xhr_projectupdate'),
244
245 url(r'^xhr_setdefaultimage/project/(?P<pid>\d+)$',
246 api.XhrSetDefaultImageUrl.as_view(),
247 name='xhr_setdefaultimage'),
248
236 url(r'xhr_project/(?P<project_id>\d+)$', 249 url(r'xhr_project/(?P<project_id>\d+)$',
237 api.XhrProject.as_view(), 250 api.XhrProject.as_view(),
238 name='xhr_project'), 251 name='xhr_project'),
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 34ed2b2e3c..4939b6b1f4 100755..100644
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -25,6 +25,7 @@ import re
25from django.db.models import F, Q, Sum 25from django.db.models import F, Q, Sum
26from django.db import IntegrityError 26from django.db import IntegrityError
27from django.shortcuts import render, redirect, get_object_or_404 27from django.shortcuts import render, redirect, get_object_or_404
28from django.utils.http import urlencode
28from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe 29from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe
29from orm.models import LogMessage, Variable, Package_Dependency, Package 30from orm.models import LogMessage, Variable, Package_Dependency, Package
30from orm.models import Task_Dependency, Package_File 31from orm.models import Task_Dependency, Package_File
@@ -51,6 +52,7 @@ logger = logging.getLogger("toaster")
51 52
52# Project creation and managed build enable 53# Project creation and managed build enable
53project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER')) 54project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER'))
55is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC'))
54 56
55class MimeTypeFinder(object): 57class MimeTypeFinder(object):
56 # setting this to False enables additional non-standard mimetypes 58 # setting this to False enables additional non-standard mimetypes
@@ -70,6 +72,7 @@ class MimeTypeFinder(object):
70# single point to add global values into the context before rendering 72# single point to add global values into the context before rendering
71def toaster_render(request, page, context): 73def toaster_render(request, page, context):
72 context['project_enable'] = project_enable 74 context['project_enable'] = project_enable
75 context['project_specific'] = is_project_specific
73 return render(request, page, context) 76 return render(request, page, context)
74 77
75 78
@@ -1434,12 +1437,160 @@ if True:
1434 1437
1435 raise Exception("Invalid HTTP method for this page") 1438 raise Exception("Invalid HTTP method for this page")
1436 1439
1440 # new project
1441 def newproject_specific(request, pid):
1442 if not project_enable:
1443 return redirect( landing )
1444
1445 project = Project.objects.get(pk=pid)
1446 template = "newproject_specific.html"
1447 context = {
1448 'email': request.user.email if request.user.is_authenticated() else '',
1449 'username': request.user.username if request.user.is_authenticated() else '',
1450 'releases': Release.objects.order_by("description"),
1451 'projectname': project.name,
1452 'project_pk': project.pk,
1453 }
1454
1455 # WORKAROUND: if we already know release, redirect 'newproject_specific' to 'project_specific'
1456 if '1' == project.get_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE'):
1457 return redirect(reverse(project_specific, args=(project.pk,)))
1458
1459 try:
1460 context['defaultbranch'] = ToasterSetting.objects.get(name = "DEFAULT_RELEASE").value
1461 except ToasterSetting.DoesNotExist:
1462 pass
1463
1464 if request.method == "GET":
1465 # render new project page
1466 return toaster_render(request, template, context)
1467 elif request.method == "POST":
1468 mandatory_fields = ['projectname', 'ptype']
1469 try:
1470 ptype = request.POST.get('ptype')
1471 if ptype == "build":
1472 mandatory_fields.append('projectversion')
1473 # make sure we have values for all mandatory_fields
1474 missing = [field for field in mandatory_fields if len(request.POST.get(field, '')) == 0]
1475 if missing:
1476 # set alert for missing fields
1477 raise BadParameterException("Fields missing: %s" % ", ".join(missing))
1478
1479 if not request.user.is_authenticated():
1480 user = authenticate(username = request.POST.get('username', '_anonuser'), password = 'nopass')
1481 if user is None:
1482 user = User.objects.create_user(username = request.POST.get('username', '_anonuser'), email = request.POST.get('email', ''), password = "nopass")
1483
1484 user = authenticate(username = user.username, password = 'nopass')
1485 login(request, user)
1486
1487 # save the project
1488 if ptype == "analysis":
1489 release = None
1490 else:
1491 release = Release.objects.get(pk = request.POST.get('projectversion', None ))
1492
1493 prj = Project.objects.create_project(name = request.POST['projectname'], release = release, existing_project = project)
1494 prj.user_id = request.user.pk
1495 prj.save()
1496 return redirect(reverse(project_specific, args=(prj.pk,)) + "?notify=new-project")
1497
1498 except (IntegrityError, BadParameterException) as e:
1499 # fill in page with previously submitted values
1500 for field in mandatory_fields:
1501 context.__setitem__(field, request.POST.get(field, "-- missing"))
1502 if isinstance(e, IntegrityError) and "username" in str(e):
1503 context['alert'] = "Your chosen username is already used"
1504 else:
1505 context['alert'] = str(e)
1506 return toaster_render(request, template, context)
1507
1508 raise Exception("Invalid HTTP method for this page")
1509
1437 # Shows the edit project page 1510 # Shows the edit project page
1438 def project(request, pid): 1511 def project(request, pid):
1439 project = Project.objects.get(pk=pid) 1512 project = Project.objects.get(pk=pid)
1513
1514 if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
1515 if request.GET:
1516 #Example:request.GET=<QueryDict: {'setMachine': ['qemuarm']}>
1517 params = urlencode(request.GET).replace('%5B%27','').replace('%27%5D','')
1518 return redirect("%s?%s" % (reverse(project_specific, args=(project.pk,)),params))
1519 else:
1520 return redirect(reverse(project_specific, args=(project.pk,)))
1440 context = {"project": project} 1521 context = {"project": project}
1441 return toaster_render(request, "project.html", context) 1522 return toaster_render(request, "project.html", context)
1442 1523
1524 # Shows the edit project-specific page
1525 def project_specific(request, pid):
1526 project = Project.objects.get(pk=pid)
1527
1528 # Are we refreshing from a successful project specific update clone?
1529 if Project.PROJECT_SPECIFIC_CLONING_SUCCESS == project.get_variable(Project.PROJECT_SPECIFIC_STATUS):
1530 return redirect(reverse(landing_specific,args=(project.pk,)))
1531
1532 context = {
1533 "project": project,
1534 "is_new" : project.get_variable(Project.PROJECT_SPECIFIC_ISNEW),
1535 "default_image_recipe" : project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE),
1536 "mru" : Build.objects.all().filter(project=project,outcome=Build.IN_PROGRESS),
1537 }
1538 if project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0:
1539 context['build_in_progress_none_completed'] = True
1540 else:
1541 context['build_in_progress_none_completed'] = False
1542 return toaster_render(request, "project.html", context)
1543
1544 # perform the final actions for the project specific page
1545 def project_specific_finalize(cmnd, pid):
1546 project = Project.objects.get(pk=pid)
1547 callback = project.get_variable(Project.PROJECT_SPECIFIC_CALLBACK)
1548 if "update" == cmnd:
1549 # Delete all '_PROJECT_PREPARE_' builds
1550 for b in Build.objects.all().filter(project=project):
1551 delete_build = False
1552 for t in b.target_set.all():
1553 if '_PROJECT_PREPARE_' == t.target:
1554 delete_build = True
1555 if delete_build:
1556 from django.core import management
1557 management.call_command('builddelete', str(b.id), interactive=False)
1558 # perform callback at this last moment if defined, in case Toaster gets shutdown next
1559 default_target = project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE)
1560 if callback:
1561 callback = callback.replace("<IMAGE>",default_target)
1562 if "cancel" == cmnd:
1563 if callback:
1564 callback = callback.replace("<IMAGE>","none")
1565 callback = callback.replace("--update","--cancel")
1566 # perform callback at this last moment if defined, in case this Toaster gets shutdown next
1567 ret = ''
1568 if callback:
1569 ret = os.system('bash -c "%s"' % callback)
1570 project.set_variable(Project.PROJECT_SPECIFIC_CALLBACK,'')
1571 # Delete the temp project specific variables
1572 project.set_variable(Project.PROJECT_SPECIFIC_ISNEW,'')
1573 project.set_variable(Project.PROJECT_SPECIFIC_STATUS,Project.PROJECT_SPECIFIC_NONE)
1574 # WORKAROUND: Release this workaround flag
1575 project.set_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE','')
1576
1577 # Shows the final landing page for project specific update
1578 def landing_specific(request, pid):
1579 project_specific_finalize("update", pid)
1580 context = {
1581 "install_dir": os.environ['TOASTER_DIR'],
1582 }
1583 return toaster_render(request, "landing_specific.html", context)
1584
1585 # Shows the related landing-specific page
1586 def landing_specific_cancel(request, pid):
1587 project_specific_finalize("cancel", pid)
1588 context = {
1589 "install_dir": os.environ['TOASTER_DIR'],
1590 "status": "cancel",
1591 }
1592 return toaster_render(request, "landing_specific.html", context)
1593
1443 def jsunittests(request): 1594 def jsunittests(request):
1444 """ Provides a page for the js unit tests """ 1595 """ Provides a page for the js unit tests """
1445 bbv = BitbakeVersion.objects.filter(branch="master").first() 1596 bbv = BitbakeVersion.objects.filter(branch="master").first()
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index a1792d997f..88dff8a857 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -89,6 +89,10 @@ class ToasterTable(TemplateView):
89 89
90 # global variables 90 # global variables
91 context['project_enable'] = ('1' == os.environ.get('TOASTER_BUILDSERVER')) 91 context['project_enable'] = ('1' == os.environ.get('TOASTER_BUILDSERVER'))
92 try:
93 context['project_specific'] = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC'))
94 except:
95 context['project_specific'] = ''
92 96
93 return context 97 return context
94 98
@@ -519,6 +523,8 @@ class MostRecentBuildsView(View):
519 int((build_obj.repos_cloned / 523 int((build_obj.repos_cloned /
520 build_obj.repos_to_clone) * 100) 524 build_obj.repos_to_clone) * 100)
521 525
526 build['progress_item'] = build_obj.progress_item
527
522 tasks_complete_percentage = 0 528 tasks_complete_percentage = 0
523 if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED): 529 if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED):
524 tasks_complete_percentage = 100 530 tasks_complete_percentage = 100