aboutsummaryrefslogtreecommitdiffstats
path: root/lib/toaster/toastergui
diff options
context:
space:
mode:
Diffstat (limited to 'lib/toaster/toastergui')
-rw-r--r--lib/toaster/toastergui/api.py168
-rw-r--r--lib/toaster/toastergui/static/js/layerBtn.js12
-rw-r--r--lib/toaster/toastergui/static/js/libtoaster.js105
-rw-r--r--lib/toaster/toastergui/static/js/mrbsection.js4
-rw-r--r--lib/toaster/toastergui/static/js/projecttopbar.js22
-rw-r--r--lib/toaster/toastergui/tables.py4
-rw-r--r--lib/toaster/toastergui/templates/base_specific.html128
-rw-r--r--lib/toaster/toastergui/templates/baseprojectspecificpage.html48
-rw-r--r--lib/toaster/toastergui/templates/generic-toastertable-page.html2
-rw-r--r--lib/toaster/toastergui/templates/importlayer.html4
-rw-r--r--lib/toaster/toastergui/templates/landing_specific.html50
-rw-r--r--lib/toaster/toastergui/templates/layerdetails.html3
-rw-r--r--lib/toaster/toastergui/templates/mrb_section.html2
-rw-r--r--lib/toaster/toastergui/templates/newcustomimage.html4
-rw-r--r--lib/toaster/toastergui/templates/newproject_specific.html95
-rw-r--r--lib/toaster/toastergui/templates/project.html7
-rw-r--r--lib/toaster/toastergui/templates/project_specific.html162
-rw-r--r--lib/toaster/toastergui/templates/project_specific_topbar.html80
-rw-r--r--lib/toaster/toastergui/templates/projectconf.html7
-rw-r--r--lib/toaster/toastergui/templates/recipe_add_btn.html23
-rw-r--r--lib/toaster/toastergui/urls.py13
-rw-r--r--[-rwxr-xr-x]lib/toaster/toastergui/views.py151
-rw-r--r--lib/toaster/toastergui/widgets.py6
23 files changed, 1086 insertions, 14 deletions
diff --git a/lib/toaster/toastergui/api.py b/lib/toaster/toastergui/api.py
index ab6ba69e0..1bec56d46 100644
--- a/lib/toaster/toastergui/api.py
+++ b/lib/toaster/toastergui/api.py
@@ -22,7 +22,9 @@ import os
import re
import logging
import json
+import subprocess
from collections import Counter
+from shutil import copyfile
from orm.models import Project, ProjectTarget, Build, Layer_Version
from orm.models import LayerVersionDependency, LayerSource, ProjectLayer
@@ -38,6 +40,18 @@ from django.core.urlresolvers import reverse
from django.db.models import Q, F
from django.db import Error
from toastergui.templatetags.projecttags import filtered_filesizeformat
+from django.utils import timezone
+import pytz
+
+# development/debugging support
+verbose = 2
+def _log(msg):
+ if 1 == verbose:
+ print(msg)
+ elif 2 == verbose:
+ f1=open('/tmp/toaster.log', 'a')
+ f1.write("|" + msg + "|\n" )
+ f1.close()
logger = logging.getLogger("toaster")
@@ -137,6 +151,130 @@ class XhrBuildRequest(View):
return response
+class XhrProjectUpdate(View):
+
+ def get(self, request, *args, **kwargs):
+ return HttpResponse()
+
+ def post(self, request, *args, **kwargs):
+ """
+ Project Update
+
+ Entry point: /xhr_projectupdate/<project_id>
+ Method: POST
+
+ Args:
+ pid: pid of project to update
+
+ Returns:
+ {"error": "ok"}
+ or
+ {"error": <error message>}
+ """
+
+ project = Project.objects.get(pk=kwargs['pid'])
+ logger.debug("ProjectUpdateCallback:project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
+
+ if 'do_update' in request.POST:
+
+ # Extract any default image recipe
+ if 'default_image' in request.POST:
+ project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,str(request.POST['default_image']))
+ else:
+ project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,'')
+
+ logger.debug("ProjectUpdateCallback:Chain to the build request")
+
+ # Chain to the build request
+ xhrBuildRequest = XhrBuildRequest()
+ return xhrBuildRequest.post(request, *args, **kwargs)
+
+ logger.warning("ERROR:XhrProjectUpdate")
+ response = HttpResponse()
+ response.status_code = 500
+ return response
+
+class XhrSetDefaultImageUrl(View):
+
+ def get(self, request, *args, **kwargs):
+ return HttpResponse()
+
+ def post(self, request, *args, **kwargs):
+ """
+ Project Update
+
+ Entry point: /xhr_setdefaultimage/<project_id>
+ Method: POST
+
+ Args:
+ pid: pid of project to update default image
+
+ Returns:
+ {"error": "ok"}
+ or
+ {"error": <error message>}
+ """
+
+ project = Project.objects.get(pk=kwargs['pid'])
+ logger.debug("XhrSetDefaultImageUrl:project.pk=%d" % (project.pk))
+
+ # set any default image recipe
+ if 'targets' in request.POST:
+ default_target = str(request.POST['targets'])
+ project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,default_target)
+ logger.debug("XhrSetDefaultImageUrl,project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
+ return error_response('ok')
+
+ logger.warning("ERROR:XhrSetDefaultImageUrl")
+ response = HttpResponse()
+ response.status_code = 500
+ return response
+
+
+#
+# Layer Management
+#
+# Rules for 'local_source_dir' layers
+# * Layers must have a unique name in the Layers table
+# * A 'local_source_dir' layer is supposed to be shared
+# by all projects that use it, so that it can have the
+# same logical name
+# * Each project that uses a layer will have its own
+# LayerVersion and Project Layer for it
+# * During the Paroject delete process, when the last
+# LayerVersion for a 'local_source_dir' layer is deleted
+# then the Layer record is deleted to remove orphans
+#
+
+def scan_layer_content(layer,layer_version):
+ # if this is a local layer directory, we can immediately scan its content
+ if layer.local_source_dir:
+ try:
+ # recipes-*/*/*.bb
+ cmd = '%s %s' % ('ls', os.path.join(layer.local_source_dir,'recipes-*/*/*.bb'))
+ recipes_list = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.read()
+ recipes_list = recipes_list.decode("utf-8").strip()
+ if recipes_list and 'No such' not in recipes_list:
+ for recipe in recipes_list.split('\n'):
+ recipe_path = recipe[recipe.rfind('recipes-'):]
+ recipe_name = recipe[recipe.rfind('/')+1:].replace('.bb','')
+ recipe_ver = recipe_name.rfind('_')
+ if recipe_ver > 0:
+ recipe_name = recipe_name[0:recipe_ver]
+ if recipe_name:
+ ro, created = Recipe.objects.get_or_create(
+ layer_version=layer_version,
+ name=recipe_name
+ )
+ if created:
+ ro.file_path = recipe_path
+ ro.summary = 'Recipe %s from layer %s' % (recipe_name,layer.name)
+ ro.description = ro.summary
+ ro.save()
+
+ except Exception as e:
+ logger.warning("ERROR:scan_layer_content: %s" % e)
+
class XhrLayer(View):
""" Delete, Get, Add and Update Layer information
@@ -265,6 +403,7 @@ class XhrLayer(View):
(csv)]
"""
+
try:
project = Project.objects.get(pk=kwargs['pid'])
@@ -285,7 +424,13 @@ class XhrLayer(View):
if layer_data['name'] in existing_layers:
return JsonResponse({"error": "layer-name-exists"})
- layer = Layer.objects.create(name=layer_data['name'])
+ if ('local_source_dir' in layer_data):
+ # Local layer can be shared across projects. They have no 'release'
+ # and are not included in get_all_compatible_layer_versions() above
+ layer,created = Layer.objects.get_or_create(name=layer_data['name'])
+ _log("Local Layer created=%s" % created)
+ else:
+ layer = Layer.objects.create(name=layer_data['name'])
layer_version = Layer_Version.objects.create(
layer=layer,
@@ -293,7 +438,7 @@ class XhrLayer(View):
layer_source=LayerSource.TYPE_IMPORTED)
# Local layer
- if ('local_source_dir' in layer_data) and layer.local_source_dir:
+ if ('local_source_dir' in layer_data): ### and layer.local_source_dir:
layer.local_source_dir = layer_data['local_source_dir']
# git layer
elif 'vcs_url' in layer_data:
@@ -325,6 +470,9 @@ class XhrLayer(View):
'layerdetailurl':
layer_dep.get_detailspage_url(project.pk)})
+ # Scan the layer's content and update components
+ scan_layer_content(layer,layer_version)
+
except Layer_Version.DoesNotExist:
return error_response("layer-dep-not-found")
except Project.DoesNotExist:
@@ -1014,8 +1162,24 @@ class XhrProject(View):
state=BuildRequest.REQ_INPROGRESS):
XhrBuildRequest.cancel_build(br)
+ # gather potential orphaned local layers attached to this project
+ project_local_layer_list = []
+ for pl in ProjectLayer.objects.filter(project=project):
+ if pl.layercommit.layer_source == LayerSource.TYPE_IMPORTED:
+ project_local_layer_list.append(pl.layercommit.layer)
+
+ # deep delete the project and its dependencies
project.delete()
+ # delete any local layers now orphaned
+ _log("LAYER_ORPHAN_CHECK:Check for orphaned layers")
+ for layer in project_local_layer_list:
+ layer_refs = Layer_Version.objects.filter(layer=layer)
+ _log("LAYER_ORPHAN_CHECK:Ref Count for '%s' = %d" % (layer.name,len(layer_refs)))
+ if 0 == len(layer_refs):
+ _log("LAYER_ORPHAN_CHECK:DELETE orpahned '%s'" % (layer.name))
+ Layer.objects.filter(pk=layer.id).delete()
+
except Project.DoesNotExist:
return error_response("Project %s does not exist" %
kwargs['project_id'])
diff --git a/lib/toaster/toastergui/static/js/layerBtn.js b/lib/toaster/toastergui/static/js/layerBtn.js
index 9f9eda1e1..a5a6563d1 100644
--- a/lib/toaster/toastergui/static/js/layerBtn.js
+++ b/lib/toaster/toastergui/static/js/layerBtn.js
@@ -67,6 +67,18 @@ function layerBtnsInit() {
});
});
+ $("td .set-default-recipe-btn").unbind('click');
+ $("td .set-default-recipe-btn").click(function(e){
+ e.preventDefault();
+ var recipe = $(this).data('recipe-name');
+
+ libtoaster.setDefaultImage(null, recipe,
+ function(){
+ /* Success */
+ window.location.replace(libtoaster.ctx.projectSpecificPageUrl);
+ });
+ });
+
$(".customise-btn").unbind('click');
$(".customise-btn").click(function(e){
diff --git a/lib/toaster/toastergui/static/js/libtoaster.js b/lib/toaster/toastergui/static/js/libtoaster.js
index 6f9b5d0f0..2e8863af2 100644
--- a/lib/toaster/toastergui/static/js/libtoaster.js
+++ b/lib/toaster/toastergui/static/js/libtoaster.js
@@ -465,6 +465,108 @@ var libtoaster = (function () {
$.cookie('toaster-notification', JSON.stringify(data), { path: '/'});
}
+ /* _updateProject:
+ * url: xhrProjectUpdateUrl or null for current project
+ * onsuccess: callback for successful execution
+ * onfail: callback for failed execution
+ */
+ function _updateProject (url, targets, default_image, onsuccess, onfail) {
+
+ if (!url)
+ url = libtoaster.ctx.xhrProjectUpdateUrl;
+
+ /* Flatten the array of targets into a space spearated list */
+ if (targets instanceof Array){
+ targets = targets.reduce(function(prevV, nextV){
+ return prev + ' ' + next;
+ });
+ }
+
+ $.ajax( {
+ type: "POST",
+ url: url,
+ data: { 'do_update' : 'True' , 'targets' : targets , 'default_image' : default_image , },
+ headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
+ success: function (_data) {
+ if (_data.error !== "ok") {
+ console.warn(_data.error);
+ } else {
+ if (onsuccess !== undefined) onsuccess(_data);
+ }
+ },
+ error: function (_data) {
+ console.warn("Call failed");
+ console.warn(_data);
+ if (onfail) onfail(data);
+ } });
+ }
+
+ /* _cancelProject:
+ * url: xhrProjectUpdateUrl or null for current project
+ * onsuccess: callback for successful execution
+ * onfail: callback for failed execution
+ */
+ function _cancelProject (url, onsuccess, onfail) {
+
+ if (!url)
+ url = libtoaster.ctx.xhrProjectCancelUrl;
+
+ $.ajax( {
+ type: "POST",
+ url: url,
+ data: { 'do_cancel' : 'True' },
+ headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
+ success: function (_data) {
+ if (_data.error !== "ok") {
+ console.warn(_data.error);
+ } else {
+ if (onsuccess !== undefined) onsuccess(_data);
+ }
+ },
+ error: function (_data) {
+ console.warn("Call failed");
+ console.warn(_data);
+ if (onfail) onfail(data);
+ } });
+ }
+
+ /* _setDefaultImage:
+ * url: xhrSetDefaultImageUrl or null for current project
+ * targets: an array or space separated list of targets to set as default
+ * onsuccess: callback for successful execution
+ * onfail: callback for failed execution
+ */
+ function _setDefaultImage (url, targets, onsuccess, onfail) {
+
+ if (!url)
+ url = libtoaster.ctx.xhrSetDefaultImageUrl;
+
+ /* Flatten the array of targets into a space spearated list */
+ if (targets instanceof Array){
+ targets = targets.reduce(function(prevV, nextV){
+ return prev + ' ' + next;
+ });
+ }
+
+ $.ajax( {
+ type: "POST",
+ url: url,
+ data: { 'targets' : targets },
+ headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
+ success: function (_data) {
+ if (_data.error !== "ok") {
+ console.warn(_data.error);
+ } else {
+ if (onsuccess !== undefined) onsuccess(_data);
+ }
+ },
+ error: function (_data) {
+ console.warn("Call failed");
+ console.warn(_data);
+ if (onfail) onfail(data);
+ } });
+ }
+
return {
enableAjaxLoadingTimer: _enableAjaxLoadingTimer,
disableAjaxLoadingTimer: _disableAjaxLoadingTimer,
@@ -485,6 +587,9 @@ var libtoaster = (function () {
createCustomRecipe: _createCustomRecipe,
makeProjectNameValidation: _makeProjectNameValidation,
setNotification: _setNotification,
+ updateProject : _updateProject,
+ cancelProject : _cancelProject,
+ setDefaultImage : _setDefaultImage,
};
})();
diff --git a/lib/toaster/toastergui/static/js/mrbsection.js b/lib/toaster/toastergui/static/js/mrbsection.js
index c0c5fa958..f07ccf818 100644
--- a/lib/toaster/toastergui/static/js/mrbsection.js
+++ b/lib/toaster/toastergui/static/js/mrbsection.js
@@ -86,7 +86,7 @@ function mrbSectionInit(ctx){
if (buildFinished(build)) {
// a build finished: reload the whole page so that the build
// shows up in the builds table
- window.location.reload();
+ window.location.reload(true);
}
else if (stateChanged(build)) {
// update the whole template
@@ -110,6 +110,8 @@ function mrbSectionInit(ctx){
// update the clone progress text
selector = '#repos-cloned-percentage-' + build.id;
$(selector).html(build.repos_cloned_percentage);
+ selector = '#repos-cloned-progressitem-' + build.id;
+ $(selector).html('('+build.progress_item+')');
// update the recipe progress bar
selector = '#repos-cloned-percentage-bar-' + build.id;
diff --git a/lib/toaster/toastergui/static/js/projecttopbar.js b/lib/toaster/toastergui/static/js/projecttopbar.js
index 69220aaf5..3f9e18670 100644
--- a/lib/toaster/toastergui/static/js/projecttopbar.js
+++ b/lib/toaster/toastergui/static/js/projecttopbar.js
@@ -14,6 +14,9 @@ function projectTopBarInit(ctx) {
var newBuildTargetBuildBtn = $("#build-button");
var selectedTarget;
+ var updateProjectBtn = $("#update-project-button");
+ var cancelProjectBtn = $("#cancel-project-button");
+
/* Project name change functionality */
projectNameFormToggle.click(function(e){
e.preventDefault();
@@ -89,6 +92,25 @@ function projectTopBarInit(ctx) {
}, null);
});
+ updateProjectBtn.click(function (e) {
+ e.preventDefault();
+
+ selectedTarget = { name: "_PROJECT_PREPARE_" };
+
+ /* Save current default build image, fire off the build */
+ libtoaster.updateProject(null, selectedTarget.name, newBuildTargetInput.val().trim(),
+ function(){
+ window.location.replace(libtoaster.ctx.projectSpecificPageUrl);
+ }, null);
+ });
+
+ cancelProjectBtn.click(function (e) {
+ e.preventDefault();
+
+ /* redirect to 'done/canceled' landing page */
+ window.location.replace(libtoaster.ctx.landingSpecificCancelURL);
+ });
+
/* Call makeProjectNameValidation function */
libtoaster.makeProjectNameValidation($("#project-name-change-input"),
$("#hint-error-project-name"), $("#validate-project-name"),
diff --git a/lib/toaster/toastergui/tables.py b/lib/toaster/toastergui/tables.py
index dca2fa291..03bd2ae9c 100644
--- a/lib/toaster/toastergui/tables.py
+++ b/lib/toaster/toastergui/tables.py
@@ -35,6 +35,8 @@ from toastergui.tablefilter import TableFilterActionToggle
from toastergui.tablefilter import TableFilterActionDateRange
from toastergui.tablefilter import TableFilterActionDay
+import os
+
class ProjectFilters(object):
@staticmethod
def in_project(project_layers):
@@ -339,6 +341,8 @@ class RecipesTable(ToasterTable):
'filter_name' : "in_current_project",
'static_data_name' : "add-del-layers",
'static_data_template' : '{% include "recipe_btn.html" %}'}
+ if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
+ build_col['static_data_template'] = '{% include "recipe_add_btn.html" %}'
def get_context_data(self, **kwargs):
project = Project.objects.get(pk=kwargs['pid'])
diff --git a/lib/toaster/toastergui/templates/base_specific.html b/lib/toaster/toastergui/templates/base_specific.html
new file mode 100644
index 000000000..e377cadd7
--- /dev/null
+++ b/lib/toaster/toastergui/templates/base_specific.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+{% load static %}
+{% load projecttags %}
+{% load project_url_tag %}
+<html lang="en">
+ <head>
+ <title>
+ {% block title %} Toaster {% endblock %}
+ </title>
+ <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" type="text/css"/>
+ <!--link rel="stylesheet" href="{% static 'css/bootstrap-theme.css' %}" type="text/css"/-->
+ <link rel="stylesheet" href="{% static 'css/font-awesome.min.css' %}" type='text/css'/>
+ <link rel="stylesheet" href="{% static 'css/default.css' %}" type='text/css'/>
+
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
+ <script src="{% static 'js/jquery-2.0.3.min.js' %}">
+ </script>
+ <script src="{% static 'js/jquery.cookie.js' %}">
+ </script>
+ <script src="{% static 'js/bootstrap.min.js' %}">
+ </script>
+ <script src="{% static 'js/typeahead.jquery.js' %}">
+ </script>
+ <script src="{% static 'js/jsrender.min.js' %}">
+ </script>
+ <script src="{% static 'js/highlight.pack.js' %}">
+ </script>
+ <script src="{% static 'js/libtoaster.js' %}">
+ </script>
+ {% if DEBUG %}
+ <script>
+ libtoaster.debug = true;
+ </script>
+ {% endif %}
+ <script>
+ /* Set JsRender delimiters (mrb_section.html) different than Django's */
+ $.views.settings.delimiters("<%", "%>");
+
+ /* This table allows Django substitutions to be passed to libtoaster.js */
+ libtoaster.ctx = {
+ jsUrl : "{% static 'js/' %}",
+ htmlUrl : "{% static 'html/' %}",
+ projectsUrl : "{% url 'all-projects' %}",
+ projectsTypeAheadUrl: {% url 'xhr_projectstypeahead' as prjurl%}{{prjurl|json}},
+ {% if project.id %}
+ landingSpecificURL : "{% url 'landing_specific' project.id %}",
+ landingSpecificCancelURL : "{% url 'landing_specific_cancel' project.id %}",
+ projectId : {{project.id}},
+ projectPageUrl : {% url 'project' project.id as purl %}{{purl|json}},
+ projectSpecificPageUrl : {% url 'project_specific' project.id as purl %}{{purl|json}},
+ xhrProjectUrl : {% url 'xhr_project' project.id as pxurl %}{{pxurl|json}},
+ projectName : {{project.name|json}},
+ recipesTypeAheadUrl: {% url 'xhr_recipestypeahead' project.id as paturl%}{{paturl|json}},
+ layersTypeAheadUrl: {% url 'xhr_layerstypeahead' project.id as paturl%}{{paturl|json}},
+ machinesTypeAheadUrl: {% url 'xhr_machinestypeahead' project.id as paturl%}{{paturl|json}},
+ distrosTypeAheadUrl: {% url 'xhr_distrostypeahead' project.id as paturl%}{{paturl|json}},
+ projectBuildsUrl: {% url 'projectbuilds' project.id as pburl %}{{pburl|json}},
+ xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}",
+ projectId : {{project.id}},
+ xhrBuildRequestUrl: "{% url 'xhr_buildrequest' project.id %}",
+ mostRecentBuildsUrl: "{% url 'most_recent_builds' %}?project_id={{project.id}}",
+ xhrProjectUpdateUrl: "{% url 'xhr_projectupdate' project.id %}",
+ xhrProjectCancelUrl: "{% url 'landing_specific_cancel' project.id %}",
+ xhrSetDefaultImageUrl: "{% url 'xhr_setdefaultimage' project.id %}",
+ {% else %}
+ mostRecentBuildsUrl: "{% url 'most_recent_builds' %}",
+ projectId : undefined,
+ projectPageUrl : undefined,
+ projectName : undefined,
+ {% endif %}
+ };
+ </script>
+ {% block extraheadcontent %}
+ {% endblock %}
+ </head>
+
+ <body>
+
+ {% csrf_token %}
+ <div id="loading-notification" class="alert alert-warning lead text-center" style="display:none">
+ Loading <i class="fa-pulse icon-spinner"></i>
+ </div>
+
+ <div id="change-notification" class="alert alert-info alert-dismissible change-notification" style="display:none">
+ <button type="button" class="close" id="hide-alert" data-toggle="alert">&times;</button>
+ <span id="change-notification-msg"></span>
+ </div>
+
+ <nav class="navbar navbar-default navbar-fixed-top">
+ <div class="container-fluid">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#global-nav" aria-expanded="false">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <div class="toaster-navbar-brand">
+ {% if project_specific %}
+ <img class="logo" src="{% static 'img/logo.png' %}" class="" alt="Yocto Project logo"/>
+ Toaster
+ {% else %}
+ <a href="/">
+ </a>
+ <a href="/">
+ <img class="logo" src="{% static 'img/logo.png' %}" class="" alt="Yocto Project logo"/>
+ </a>
+ <a class="brand" href="/">Toaster</a>
+ {% endif %}
+ {% if DEBUG %}
+ <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>
+ {% endif %}
+ </div>
+ </div>
+ <div class="collapse navbar-collapse" id="global-nav">
+ <ul class="nav navbar-nav">
+ <h3> Project Configuration Page </h3>
+ </div>
+ </div>
+ </nav>
+
+ <div class="container-fluid">
+ {% block pagecontent %}
+ {% endblock %}
+ </div>
+ </body>
+</html>
diff --git a/lib/toaster/toastergui/templates/baseprojectspecificpage.html b/lib/toaster/toastergui/templates/baseprojectspecificpage.html
new file mode 100644
index 000000000..d0b588de9
--- /dev/null
+++ b/lib/toaster/toastergui/templates/baseprojectspecificpage.html
@@ -0,0 +1,48 @@
+{% extends "base_specific.html" %}
+
+{% load projecttags %}
+{% load humanize %}
+
+{% block title %} {{title}} - {{project.name}} - Toaster {% endblock %}
+
+{% block pagecontent %}
+
+<div class="row">
+ {% include "project_specific_topbar.html" %}
+ <script type="text/javascript">
+$(document).ready(function(){
+ $("#config-nav .nav li a").each(function(){
+ if (window.location.pathname === $(this).attr('href'))
+ $(this).parent().addClass('active');
+ else
+ $(this).parent().removeClass('active');
+ });
+
+ $("#topbar-configuration-tab").addClass("active")
+ });
+ </script>
+
+ <!-- only on config pages -->
+ <div id="config-nav" class="col-md-2">
+ <ul class="nav nav-pills nav-stacked">
+ <li><a class="nav-parent" href="{% url 'project' project.id %}">Configuration</a></li>
+ <li class="nav-header">Compatible metadata</li>
+ <li><a href="{% url 'projectcustomimages' project.id %}">Custom images</a></li>
+ <li><a href="{% url 'projectimagerecipes' project.id %}">Image recipes</a></li>
+ <li><a href="{% url 'projectsoftwarerecipes' project.id %}">Software recipes</a></li>
+ <li><a href="{% url 'projectmachines' project.id %}">Machines</a></li>
+ <li><a href="{% url 'projectlayers' project.id %}">Layers</a></li>
+ <li><a href="{% url 'projectdistros' project.id %}">Distros</a></li>
+ <li class="nav-header">Extra configuration</li>
+ <li><a href="{% url 'projectconf' project.id %}">BitBake variables</a></li>
+
+ <li class="nav-header">Actions</li>
+ </ul>
+ </div>
+ <div class="col-md-10">
+ {% block projectinfomain %}{% endblock %}
+ </div>
+
+</div>
+{% endblock %}
+
diff --git a/lib/toaster/toastergui/templates/generic-toastertable-page.html b/lib/toaster/toastergui/templates/generic-toastertable-page.html
index b3eabe1a2..99fbb3897 100644
--- a/lib/toaster/toastergui/templates/generic-toastertable-page.html
+++ b/lib/toaster/toastergui/templates/generic-toastertable-page.html
@@ -1,4 +1,4 @@
-{% extends "baseprojectpage.html" %}
+{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %}
{% load projecttags %}
{% load humanize %}
{% load static %}
diff --git a/lib/toaster/toastergui/templates/importlayer.html b/lib/toaster/toastergui/templates/importlayer.html
index 97d52c76c..e0c987eef 100644
--- a/lib/toaster/toastergui/templates/importlayer.html
+++ b/lib/toaster/toastergui/templates/importlayer.html
@@ -1,4 +1,4 @@
-{% extends "base.html" %}
+{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %}
{% load projecttags %}
{% load humanize %}
{% load static %}
@@ -6,7 +6,7 @@
{% block pagecontent %}
<div class="row">
- {% include "projecttopbar.html" %}
+ {% include project_specific|yesno:"project_specific_topbar.html,projecttopbar.html" %}
{% if project and project.release %}
<script src="{% static 'js/layerDepsModal.js' %}"></script>
<script src="{% static 'js/importlayer.js' %}"></script>
diff --git a/lib/toaster/toastergui/templates/landing_specific.html b/lib/toaster/toastergui/templates/landing_specific.html
new file mode 100644
index 000000000..e289c7d4a
--- /dev/null
+++ b/lib/toaster/toastergui/templates/landing_specific.html
@@ -0,0 +1,50 @@
+{% extends "base_specific.html" %}
+
+{% load static %}
+{% load projecttags %}
+{% load humanize %}
+
+{% block title %} Welcome to Toaster {% endblock %}
+
+{% block pagecontent %}
+
+ <div class="container">
+ <div class="row">
+ <!-- Empty - no build module -->
+ <div class="page-header top-air">
+ <h1>
+ Configuration {% if status == "cancel" %}Canceled{% else %}Completed{% endif %}! You can now close this window.
+ </h1>
+ </div>
+ <div class="alert alert-info lead">
+ <p>
+ Your project configuration {% if status == "cancel" %}changes have been canceled{% else %}has completed!{% endif %}
+ <br>
+ <br>
+ <ul>
+ <li>
+ The Toaster instance for project configuration has been shut down
+ </li>
+ <li>
+ You can start Toaster independently for advanced project management and analysis:
+ <pre><code>
+ Set up bitbake environment:
+ $ cd {{install_dir}}
+ $ . oe-init-build-env [toaster_server]
+
+ Option 1: Start a local Toaster server, open local browser to "localhost:8000"
+ $ . toaster start webport=8000
+
+ Option 2: Start a shared Toaster server, open any browser to "[host_ip]:8000"
+ $ . toaster start webport=0.0.0.0:8000
+
+ To stop the Toaster server:
+ $ . toaster stop
+ </code></pre>
+ </li>
+ </ul>
+ </p>
+ </div>
+ </div>
+
+{% endblock %}
diff --git a/lib/toaster/toastergui/templates/layerdetails.html b/lib/toaster/toastergui/templates/layerdetails.html
index e0069db80..1e26e31c8 100644
--- a/lib/toaster/toastergui/templates/layerdetails.html
+++ b/lib/toaster/toastergui/templates/layerdetails.html
@@ -1,4 +1,4 @@
-{% extends "base.html" %}
+{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %}
{% load projecttags %}
{% load humanize %}
{% load static %}
@@ -310,6 +310,7 @@
{% endwith %}
{% endwith %}
</div>
+
</div> <!-- end tab content -->
</div> <!-- end tabable -->
diff --git a/lib/toaster/toastergui/templates/mrb_section.html b/lib/toaster/toastergui/templates/mrb_section.html
index c5b9fe90d..98d9fac82 100644
--- a/lib/toaster/toastergui/templates/mrb_section.html
+++ b/lib/toaster/toastergui/templates/mrb_section.html
@@ -119,7 +119,7 @@
title="Toaster is cloning the repos required for your build">
</span>
- Cloning <span id="repos-cloned-percentage-<%:id%>"><%:repos_cloned_percentage%></span>% complete
+ Cloning <span id="repos-cloned-percentage-<%:id%>"><%:repos_cloned_percentage%></span>% complete <span id="repos-cloned-progressitem-<%:id%>">(<%:progress_item%>)</span>
<%include tmpl='#cancel-template'/%>
</div>
diff --git a/lib/toaster/toastergui/templates/newcustomimage.html b/lib/toaster/toastergui/templates/newcustomimage.html
index 980179a40..0766e5e4c 100644
--- a/lib/toaster/toastergui/templates/newcustomimage.html
+++ b/lib/toaster/toastergui/templates/newcustomimage.html
@@ -1,4 +1,4 @@
-{% extends "base.html" %}
+{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %}
{% load projecttags %}
{% load humanize %}
{% load static %}
@@ -8,7 +8,7 @@
<div class="row">
- {% include "projecttopbar.html" %}
+ {% include project_specific|yesno:"project_specific_topbar.html,projecttopbar.html" %}
<div class="col-md-12">
{% url table_name project.id as xhr_table_url %}
diff --git a/lib/toaster/toastergui/templates/newproject_specific.html b/lib/toaster/toastergui/templates/newproject_specific.html
new file mode 100644
index 000000000..cfa77f2e4
--- /dev/null
+++ b/lib/toaster/toastergui/templates/newproject_specific.html
@@ -0,0 +1,95 @@
+{% extends "base.html" %}
+{% load projecttags %}
+{% load humanize %}
+
+{% block title %} Create a new project - Toaster {% endblock %}
+
+{% block pagecontent %}
+<div class="row">
+ <div class="col-md-12">
+ <div class="page-header">
+ <h1>Create a new project</h1>
+ </div>
+ {% if alert %}
+ <div class="alert alert-danger" role="alert">{{alert}}</div>
+ {% endif %}
+
+ <form method="POST" action="{%url "newproject_specific" project_pk %}">{% csrf_token %}
+ <div class="form-group" id="validate-project-name">
+ <label class="control-label">Project name <span class="text-muted">(required)</span></label>
+ <input type="text" class="form-control" required id="new-project-name" name="display_projectname" value="{{projectname}}" disabled>
+ </div>
+ <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>
+ <input type="hidden" name="ptype" value="build" />
+ <input type="hidden" name="projectname" value="{{projectname}}" />
+
+ {% if releases.count > 0 %}
+ <div class="release form-group">
+ {% if releases.count > 1 %}
+ <label class="control-label">
+ Release
+ <span class="glyphicon glyphicon-question-sign get-help" title="The version of the build system you want to use"></span>
+ </label>
+ <select name="projectversion" id="projectversion" class="form-control">
+ {% for release in releases %}
+ <option value="{{release.id}}"
+ {%if defaultbranch == release.name %}
+ selected
+ {%endif%}
+ >{{release.description}}</option>
+ {% endfor %}
+ </select>
+ <div class="row">
+ <div class="col-md-4">
+ {% for release in releases %}
+ <div class="helptext" id="description-{{release.id}}" style="display: none">
+ <span class="help-block">{{release.helptext|safe}}</span>
+ </div>
+ {% endfor %}
+ {% else %}
+ <input type="hidden" name="projectversion" value="{{releases.0.id}}"/>
+ {% endif %}
+ </div>
+ </div>
+ </fieldset>
+ {% endif %}
+ <div class="top-air">
+ <input type="submit" id="create-project-button" class="btn btn-primary btn-lg" value="Create project"/>
+ <span class="help-inline" style="vertical-align:middle;">To create a project, you need to specify the release</span>
+ </div>
+
+ </form>
+ </div>
+ </div>
+
+ <script type="text/javascript">
+ $(document).ready(function () {
+ // hide the new project button, name is preset
+ $("#new-project-button").hide();
+
+ // enable submit button when all required fields are populated
+ $("input#new-project-name").on('input', function() {
+ if ($("input#new-project-name").val().length > 0 ){
+ $('.btn-primary').removeAttr('disabled');
+ $(".help-inline").css('visibility','hidden');
+ }
+ else {
+ $('.btn-primary').attr('disabled', 'disabled');
+ $(".help-inline").css('visibility','visible');
+ }
+ });
+
+ // show relevant help text for the selected release
+ var selected_release = $('select').val();
+ $("#description-" + selected_release).show();
+
+ $('select').change(function(){
+ var new_release = $('select').val();
+ $(".helptext").hide();
+ $('#description-' + new_release).fadeIn();
+ });
+
+ });
+ </script>
+
+{% endblock %}
diff --git a/lib/toaster/toastergui/templates/project.html b/lib/toaster/toastergui/templates/project.html
index 11603d1e1..fa41e3c90 100644
--- a/lib/toaster/toastergui/templates/project.html
+++ b/lib/toaster/toastergui/templates/project.html
@@ -1,4 +1,4 @@
-{% extends "baseprojectpage.html" %}
+{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %}
{% load projecttags %}
{% load humanize %}
@@ -18,7 +18,7 @@
try {
projectPageInit(ctx);
} catch (e) {
- document.write("Sorry, An error has occurred loading this page");
+ document.write("Sorry, An error has occurred loading this page (project):"+e);
console.warn(e);
}
});
@@ -93,6 +93,7 @@
</form>
</div>
+ {% if not project_specific %}
<div class="well well-transparent">
<h3>Most built recipes</h3>
@@ -105,6 +106,7 @@
</ul>
<button class="btn btn-primary" id="freq-build-btn" disabled="disabled">Build selected recipes</button>
</div>
+ {% endif %}
<div class="well well-transparent">
<h3>Project release</h3>
@@ -157,5 +159,6 @@
<ul class="list-unstyled lead" id="layers-in-project-list">
</ul>
</div>
+
</div>
{% endblock %}
diff --git a/lib/toaster/toastergui/templates/project_specific.html b/lib/toaster/toastergui/templates/project_specific.html
new file mode 100644
index 000000000..f625d18ba
--- /dev/null
+++ b/lib/toaster/toastergui/templates/project_specific.html
@@ -0,0 +1,162 @@
+{% extends "baseprojectspecificpage.html" %}
+
+{% load projecttags %}
+{% load humanize %}
+{% load static %}
+
+{% block title %} Configuration - {{project.name}} - Toaster {% endblock %}
+{% block projectinfomain %}
+
+<script src="{% static 'js/layerDepsModal.js' %}"></script>
+<script src="{% static 'js/projectpage.js' %}"></script>
+<script>
+ $(document).ready(function (){
+ var ctx = {
+ testReleaseChangeUrl: "{% url 'xhr_testreleasechange' project.id %}",
+ };
+
+ try {
+ projectPageInit(ctx);
+ } catch (e) {
+ document.write("Sorry, An error has occurred loading this page");
+ console.warn(e);
+ }
+ });
+</script>
+
+<div id="delete-project-modal" class="modal fade" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4>Are you sure you want to delete this project?</h4>
+ </div>
+ <div class="modal-body">
+ <p>Deleting the <strong class="project-name"></strong> project
+ will:</p>
+ <ul>
+ <li>Cancel its builds currently in progress</li>
+ <li>Remove its configuration information</li>
+ <li>Remove its imported layers</li>
+ <li>Remove its custom images</li>
+ <li>Remove all its build information</li>
+ </ul>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-primary" id="delete-project-confirmed">
+ <span data-role="submit-state">Delete project</span>
+ <span data-role="loading-state" style="display:none">
+ <span class="fa-pulse">
+ <i class="fa-pulse icon-spinner"></i>
+ </span>
+ &nbsp;Deleting project...
+ </span>
+ </button>
+ <button type="button" class="btn btn-link" data-dismiss="modal">Cancel</button>
+ </div>
+ </div><!-- /.modal-content -->
+ </div><!-- /.modal-dialog -->
+</div>
+
+
+<div class="row" id="project-page" style="display:none">
+ <div class="col-md-6">
+ <div class="well well-transparent" id="machine-section">
+ <h3>Machine</h3>
+
+ <p class="lead"><span id="project-machine-name"></span> <span class="glyphicon glyphicon-edit" id="change-machine-toggle"></span></p>
+
+ <form id="select-machine-form" style="display:none;" class="form-inline">
+ <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>
+ <div class="form-group" id="machine-input-form">
+ <input class="form-control" id="machine-change-input" autocomplete="off" value="" data-provide="typeahead" data-minlength="1" data-autocomplete="off" type="text">
+ </div>
+ <button id="machine-change-btn" class="btn btn-default" type="button">Save</button>
+ <a href="#" id="cancel-machine-change" class="btn btn-link">Cancel</a>
+ <span class="help-block text-danger" id="invalid-machine-name-help" style="display:none">A valid machine name cannot include spaces.</span>
+ <p class="form-link"><a href="{% url 'projectmachines' project.id %}">View compatible machines</a></p>
+ </form>
+ </div>
+
+ <div class="well well-transparent" id="distro-section">
+ <h3>Distro</h3>
+
+ <p class="lead"><span id="project-distro-name"></span> <span class="glyphicon glyphicon-edit" id="change-distro-toggle"></span></p>
+
+ <form id="select-distro-form" style="display:none;" class="form-inline">
+ <span class="help-block">Distro suggestions come from the Layer Index</a></span>
+ <div class="form-group">
+ <input class="form-control" id="distro-change-input" autocomplete="off" value="" data-provide="typeahead" data-minlength="1" data-autocomplete="off" type="text">
+ </div>
+ <button id="distro-change-btn" class="btn btn-default" type="button">Save</button>
+ <a href="#" id="cancel-distro-change" class="btn btn-link">Cancel</a>
+ <p class="form-link"><a href="{% url 'projectdistros' project.id %}">View compatible distros</a></p>
+ </form>
+ </div>
+
+ <div class="well well-transparent">
+ <h3>Most built recipes</h3>
+
+ <div class="alert alert-info" style="display:none" id="no-most-built">
+ <h4>You haven't built any recipes yet</h4>
+ <p class="form-link"><a href="{% url 'projectimagerecipes' project.id %}">Choose a recipe to build</a></p>
+ </div>
+
+ <ul class="list-unstyled lead" id="freq-build-list">
+ </ul>
+ <button class="btn btn-primary" id="freq-build-btn" disabled="disabled">Build selected recipes</button>
+ </div>
+
+ <div class="well well-transparent">
+ <h3>Project release</h3>
+
+ <p class="lead"><span id="project-release-title"></span>
+
+ <!-- Comment out the ability to change the project release, until we decide what to do with this functionality -->
+
+ <!--i title="" data-original-title="" id="release-change-toggle" class="icon-pencil"></i-->
+ </p>
+
+ <!-- Comment out the ability to change the project release, until we decide what to do with this functionality -->
+
+ <!--form class="form-inline" id="change-release-form" style="display:none;">
+ <select></select>
+ <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>
+ </form-->
+ </div>
+ </div>
+
+ <div class="col-md-6">
+ <div class="well well-transparent" id="layer-container">
+ <h3>Layers <span class="counter">(<span id="project-layers-count"></span>)</span>
+ <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>
+ </h3>
+
+ <div class="alert alert-warning" id="no-layers-in-project" style="display:none">
+ <h4>This project has no layers</h4>
+ In order to build this project you need to add some layers first. For that you can:
+ <ul>
+ <li><a href="{% url 'projectlayers' project.id %}">Choose from the layers compatible with this project</a></li>
+ <li><a href="{% url 'importlayer' project.id %}">Import a layer</a></li>
+ <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>
+ <li>Or type a layer name below</li>
+ </ul>
+ </div>
+
+ <form class="form-inline">
+ <div class="form-group">
+ <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">
+ </div>
+ <button id="add-layer-btn" class="btn btn-default" disabled>Add layer</button>
+ <p class="form-link">
+ <a href="{% url 'projectlayers' project.id %}" id="view-compatible-layers">View compatible layers</a>
+ <span class="text-muted">|</span>
+ <a href="{% url 'importlayer' project.id %}">Import layer</a>
+ </p>
+ </form>
+
+ <ul class="list-unstyled lead" id="layers-in-project-list">
+ </ul>
+ </div>
+
+</div>
+{% endblock %}
diff --git a/lib/toaster/toastergui/templates/project_specific_topbar.html b/lib/toaster/toastergui/templates/project_specific_topbar.html
new file mode 100644
index 000000000..622787c4b
--- /dev/null
+++ b/lib/toaster/toastergui/templates/project_specific_topbar.html
@@ -0,0 +1,80 @@
+{% load static %}
+<script src="{% static 'js/projecttopbar.js' %}"></script>
+<script>
+ $(document).ready(function () {
+ var ctx = {
+ numProjectLayers : {{project.get_project_layer_versions.count}},
+ machine : "{{project.get_current_machine_name|default_if_none:""}}",
+ }
+
+ try {
+ projectTopBarInit(ctx);
+ } catch (e) {
+ document.write("Sorry, An error has occurred loading this page (pstb):"+e);
+ console.warn(e);
+ }
+ });
+</script>
+
+<div class="col-md-12">
+ <div class="alert alert-success alert-dismissible change-notification" id="project-created-notification" style="display:none">
+ <button type="button" class="close" data-dismiss="alert">&times;</button>
+ <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>
+ </div>
+ <!-- project name -->
+ <div class="page-header">
+ <h1 id="project-name-container">
+ <span class="project-name">{{project.name}}</span>
+ {% if project.is_default %}
+ <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>
+ {% endif %}
+ </h1>
+ <form id="project-name-change-form" class="form-inline" style="display: none;">
+ <div class="form-group">
+ <input class="form-control input-lg" type="text" id="project-name-change-input" autocomplete="off" value="{{project.name}}">
+ </div>
+ <button id="project-name-change-btn" class="btn btn-default btn-lg" type="button">Save</button>
+ <a href="#" id="project-name-change-cancel" class="btn btn-lg btn-link">Cancel</a>
+ </form>
+ </div>
+
+ {% with mrb_type='project' %}
+ {% include "mrb_section.html" %}
+ {% endwith %}
+
+ {% if not project.is_default %}
+ <div id="project-topbar">
+ <ul class="nav nav-tabs">
+ <li id="topbar-configuration-tab">
+ <a href="{% url 'project_specific' project.id %}">
+ Configuration
+ </a>
+ </li>
+ <li>
+ <a href="{% url 'importlayer' project.id %}">
+ Import layer
+ </a>
+ </li>
+ <li>
+ <a href="{% url 'newcustomimage' project.id %}">
+ New custom image
+ </a>
+ </li>
+ <li class="pull-right">
+ <form class="form-inline">
+ <div class="form-group">
+ <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>
+ <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}}">
+ </div>
+ {% if project.get_is_new %}
+ <button id="update-project-button" class="btn btn-primary btn-lg" data-project-id="{{project.id}}">Prepare Project</button>
+ {% else %}
+ <button id="cancel-project-button" class="btn info btn-lg" data-project-id="{{project.id}}">Cancel</button>
+ <button id="update-project-button" class="btn btn-primary btn-lg" data-project-id="{{project.id}}">Update</button>
+ {% endif %}
+ </form>
+ </li>
+ </ul>
+ </div>
+ {% endif %}
+</div>
diff --git a/lib/toaster/toastergui/templates/projectconf.html b/lib/toaster/toastergui/templates/projectconf.html
index 933c588f3..fb20b26f2 100644
--- a/lib/toaster/toastergui/templates/projectconf.html
+++ b/lib/toaster/toastergui/templates/projectconf.html
@@ -1,4 +1,4 @@
-{% extends "baseprojectpage.html" %}
+{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %}
{% load projecttags %}
{% load humanize %}
@@ -438,8 +438,11 @@ function onEditPageUpdate(data) {
var_context='m';
}
}
+ if (configvars_sorted[i][0].startsWith("INTERNAL_")) {
+ var_context='m';
+ }
if (var_context == undefined) {
- 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>'
+ 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>'
orightml += '<dd class="variable-list">'
orightml += ' <span class="lead" id="config_var_value_'+configvars_sorted[i][2]+'"></span>'
orightml += ' <span class="glyphicon glyphicon-edit js-icon-pencil-config_var" x-data="'+configvars_sorted[i][2]+'"></span>'
diff --git a/lib/toaster/toastergui/templates/recipe_add_btn.html b/lib/toaster/toastergui/templates/recipe_add_btn.html
new file mode 100644
index 000000000..06c464561
--- /dev/null
+++ b/lib/toaster/toastergui/templates/recipe_add_btn.html
@@ -0,0 +1,23 @@
+<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;
+ {% if data.layer_version.pk not in extra.current_layers %}
+ display:none;
+ {% endif %}"
+ >
+ Set recipe
+</a>
+<a class="btn btn-default btn-block layerbtn layer-add-{{data.layer_version.pk}}"
+ data-layer='{
+ "id": {{data.layer_version.pk}},
+ "name": "{{data.layer_version.layer.name}}",
+ "layerdetailurl": "{%url "layerdetails" extra.pid data.layer_version.pk%}",
+ "xhrLayerUrl": "{% url "xhr_layer" extra.pid data.layer_version.pk %}"
+ }' data-directive="add"
+ {% if data.layer_version.pk in extra.current_layers %}
+ style="display:none;"
+ {% endif %}
+>
+ <span class="glyphicon glyphicon-plus"></span>
+ Add layer
+ <span class="glyphicon glyphicon-question-sign get-help" title="To set this
+ recipe you must first add the {{data.layer_version.layer.name}} layer to your project"></i>
+</a>
diff --git a/lib/toaster/toastergui/urls.py b/lib/toaster/toastergui/urls.py
index e07b0efc1..dc03e3035 100644
--- a/lib/toaster/toastergui/urls.py
+++ b/lib/toaster/toastergui/urls.py
@@ -116,6 +116,11 @@ urlpatterns = [
tables.ProjectBuildsTable.as_view(template_name="projectbuilds-toastertable.html"),
name='projectbuilds'),
+ url(r'^newproject_specific/(?P<pid>\d+)/$', views.newproject_specific, name='newproject_specific'),
+ url(r'^project_specific/(?P<pid>\d+)/$', views.project_specific, name='project_specific'),
+ url(r'^landing_specific/(?P<pid>\d+)/$', views.landing_specific, name='landing_specific'),
+ url(r'^landing_specific_cancel/(?P<pid>\d+)/$', views.landing_specific_cancel, name='landing_specific_cancel'),
+
# the import layer is a project-specific functionality;
url(r'^project/(?P<pid>\d+)/importlayer$', views.importlayer, name='importlayer'),
@@ -233,6 +238,14 @@ urlpatterns = [
api.XhrBuildRequest.as_view(),
name='xhr_buildrequest'),
+ url(r'^xhr_projectupdate/project/(?P<pid>\d+)$',
+ api.XhrProjectUpdate.as_view(),
+ name='xhr_projectupdate'),
+
+ url(r'^xhr_setdefaultimage/project/(?P<pid>\d+)$',
+ api.XhrSetDefaultImageUrl.as_view(),
+ name='xhr_setdefaultimage'),
+
url(r'xhr_project/(?P<project_id>\d+)$',
api.XhrProject.as_view(),
name='xhr_project'),
diff --git a/lib/toaster/toastergui/views.py b/lib/toaster/toastergui/views.py
index 34ed2b2e3..4939b6b1f 100755..100644
--- a/lib/toaster/toastergui/views.py
+++ b/lib/toaster/toastergui/views.py
@@ -25,6 +25,7 @@ import re
from django.db.models import F, Q, Sum
from django.db import IntegrityError
from django.shortcuts import render, redirect, get_object_or_404
+from django.utils.http import urlencode
from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe
from orm.models import LogMessage, Variable, Package_Dependency, Package
from orm.models import Task_Dependency, Package_File
@@ -51,6 +52,7 @@ logger = logging.getLogger("toaster")
# Project creation and managed build enable
project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER'))
+is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC'))
class MimeTypeFinder(object):
# setting this to False enables additional non-standard mimetypes
@@ -70,6 +72,7 @@ class MimeTypeFinder(object):
# single point to add global values into the context before rendering
def toaster_render(request, page, context):
context['project_enable'] = project_enable
+ context['project_specific'] = is_project_specific
return render(request, page, context)
@@ -1434,12 +1437,160 @@ if True:
raise Exception("Invalid HTTP method for this page")
+ # new project
+ def newproject_specific(request, pid):
+ if not project_enable:
+ return redirect( landing )
+
+ project = Project.objects.get(pk=pid)
+ template = "newproject_specific.html"
+ context = {
+ 'email': request.user.email if request.user.is_authenticated() else '',
+ 'username': request.user.username if request.user.is_authenticated() else '',
+ 'releases': Release.objects.order_by("description"),
+ 'projectname': project.name,
+ 'project_pk': project.pk,
+ }
+
+ # WORKAROUND: if we already know release, redirect 'newproject_specific' to 'project_specific'
+ if '1' == project.get_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE'):
+ return redirect(reverse(project_specific, args=(project.pk,)))
+
+ try:
+ context['defaultbranch'] = ToasterSetting.objects.get(name = "DEFAULT_RELEASE").value
+ except ToasterSetting.DoesNotExist:
+ pass
+
+ if request.method == "GET":
+ # render new project page
+ return toaster_render(request, template, context)
+ elif request.method == "POST":
+ mandatory_fields = ['projectname', 'ptype']
+ try:
+ ptype = request.POST.get('ptype')
+ if ptype == "build":
+ mandatory_fields.append('projectversion')
+ # make sure we have values for all mandatory_fields
+ missing = [field for field in mandatory_fields if len(request.POST.get(field, '')) == 0]
+ if missing:
+ # set alert for missing fields
+ raise BadParameterException("Fields missing: %s" % ", ".join(missing))
+
+ if not request.user.is_authenticated():
+ user = authenticate(username = request.POST.get('username', '_anonuser'), password = 'nopass')
+ if user is None:
+ user = User.objects.create_user(username = request.POST.get('username', '_anonuser'), email = request.POST.get('email', ''), password = "nopass")
+
+ user = authenticate(username = user.username, password = 'nopass')
+ login(request, user)
+
+ # save the project
+ if ptype == "analysis":
+ release = None
+ else:
+ release = Release.objects.get(pk = request.POST.get('projectversion', None ))
+
+ prj = Project.objects.create_project(name = request.POST['projectname'], release = release, existing_project = project)
+ prj.user_id = request.user.pk
+ prj.save()
+ return redirect(reverse(project_specific, args=(prj.pk,)) + "?notify=new-project")
+
+ except (IntegrityError, BadParameterException) as e:
+ # fill in page with previously submitted values
+ for field in mandatory_fields:
+ context.__setitem__(field, request.POST.get(field, "-- missing"))
+ if isinstance(e, IntegrityError) and "username" in str(e):
+ context['alert'] = "Your chosen username is already used"
+ else:
+ context['alert'] = str(e)
+ return toaster_render(request, template, context)
+
+ raise Exception("Invalid HTTP method for this page")
+
# Shows the edit project page
def project(request, pid):
project = Project.objects.get(pk=pid)
+
+ if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
+ if request.GET:
+ #Example:request.GET=<QueryDict: {'setMachine': ['qemuarm']}>
+ params = urlencode(request.GET).replace('%5B%27','').replace('%27%5D','')
+ return redirect("%s?%s" % (reverse(project_specific, args=(project.pk,)),params))
+ else:
+ return redirect(reverse(project_specific, args=(project.pk,)))
context = {"project": project}
return toaster_render(request, "project.html", context)
+ # Shows the edit project-specific page
+ def project_specific(request, pid):
+ project = Project.objects.get(pk=pid)
+
+ # Are we refreshing from a successful project specific update clone?
+ if Project.PROJECT_SPECIFIC_CLONING_SUCCESS == project.get_variable(Project.PROJECT_SPECIFIC_STATUS):
+ return redirect(reverse(landing_specific,args=(project.pk,)))
+
+ context = {
+ "project": project,
+ "is_new" : project.get_variable(Project.PROJECT_SPECIFIC_ISNEW),
+ "default_image_recipe" : project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE),
+ "mru" : Build.objects.all().filter(project=project,outcome=Build.IN_PROGRESS),
+ }
+ if project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0:
+ context['build_in_progress_none_completed'] = True
+ else:
+ context['build_in_progress_none_completed'] = False
+ return toaster_render(request, "project.html", context)
+
+ # perform the final actions for the project specific page
+ def project_specific_finalize(cmnd, pid):
+ project = Project.objects.get(pk=pid)
+ callback = project.get_variable(Project.PROJECT_SPECIFIC_CALLBACK)
+ if "update" == cmnd:
+ # Delete all '_PROJECT_PREPARE_' builds
+ for b in Build.objects.all().filter(project=project):
+ delete_build = False
+ for t in b.target_set.all():
+ if '_PROJECT_PREPARE_' == t.target:
+ delete_build = True
+ if delete_build:
+ from django.core import management
+ management.call_command('builddelete', str(b.id), interactive=False)
+ # perform callback at this last moment if defined, in case Toaster gets shutdown next
+ default_target = project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE)
+ if callback:
+ callback = callback.replace("<IMAGE>",default_target)
+ if "cancel" == cmnd:
+ if callback:
+ callback = callback.replace("<IMAGE>","none")
+ callback = callback.replace("--update","--cancel")
+ # perform callback at this last moment if defined, in case this Toaster gets shutdown next
+ ret = ''
+ if callback:
+ ret = os.system('bash -c "%s"' % callback)
+ project.set_variable(Project.PROJECT_SPECIFIC_CALLBACK,'')
+ # Delete the temp project specific variables
+ project.set_variable(Project.PROJECT_SPECIFIC_ISNEW,'')
+ project.set_variable(Project.PROJECT_SPECIFIC_STATUS,Project.PROJECT_SPECIFIC_NONE)
+ # WORKAROUND: Release this workaround flag
+ project.set_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE','')
+
+ # Shows the final landing page for project specific update
+ def landing_specific(request, pid):
+ project_specific_finalize("update", pid)
+ context = {
+ "install_dir": os.environ['TOASTER_DIR'],
+ }
+ return toaster_render(request, "landing_specific.html", context)
+
+ # Shows the related landing-specific page
+ def landing_specific_cancel(request, pid):
+ project_specific_finalize("cancel", pid)
+ context = {
+ "install_dir": os.environ['TOASTER_DIR'],
+ "status": "cancel",
+ }
+ return toaster_render(request, "landing_specific.html", context)
+
def jsunittests(request):
""" Provides a page for the js unit tests """
bbv = BitbakeVersion.objects.filter(branch="master").first()
diff --git a/lib/toaster/toastergui/widgets.py b/lib/toaster/toastergui/widgets.py
index a1792d997..88dff8a85 100644
--- a/lib/toaster/toastergui/widgets.py
+++ b/lib/toaster/toastergui/widgets.py
@@ -89,6 +89,10 @@ class ToasterTable(TemplateView):
# global variables
context['project_enable'] = ('1' == os.environ.get('TOASTER_BUILDSERVER'))
+ try:
+ context['project_specific'] = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC'))
+ except:
+ context['project_specific'] = ''
return context
@@ -519,6 +523,8 @@ class MostRecentBuildsView(View):
int((build_obj.repos_cloned /
build_obj.repos_to_clone) * 100)
+ build['progress_item'] = build_obj.progress_item
+
tasks_complete_percentage = 0
if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED):
tasks_complete_percentage = 100