diff options
Diffstat (limited to 'bitbake')
9 files changed, 345 insertions, 96 deletions
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py index f0a8786640..75e6ea3996 100644 --- a/bitbake/lib/toaster/orm/models.py +++ b/bitbake/lib/toaster/orm/models.py @@ -490,6 +490,47 @@ class Build(models.Model): tgts = Target.objects.filter(build_id = self.id).order_by( 'target' ); return( tgts ); + def get_recipes(self): + """ + Get the recipes related to this build; + note that the related layer versions and layers are also prefetched + by this query, as this queryset can be sorted by these objects in the + build recipes view; prefetching them here removes the need + for another query in that view + """ + layer_versions = Layer_Version.objects.filter(build=self) + criteria = Q(layer_version__id__in=layer_versions) + return Recipe.objects.filter(criteria) \ + .select_related('layer_version', 'layer_version__layer') + + def get_custom_image_recipe_names(self): + """ + Get the names of custom image recipes for this build's project + as a list; this is used to screen out custom image recipes from the + recipes for the build by name, and to distinguish image recipes from + custom image recipes + """ + custom_image_recipes = \ + CustomImageRecipe.objects.filter(project=self.project) + return custom_image_recipes.values_list('name', flat=True) + + def get_image_recipes(self): + """ + Returns a queryset of image recipes related to this build, sorted + by name + """ + criteria = Q(is_image=True) + return self.get_recipes().filter(criteria).order_by('name') + + def get_custom_image_recipes(self): + """ + Returns a queryset of custom image recipes related to this build, + sorted by name + """ + custom_image_recipe_names = self.get_custom_image_recipe_names() + criteria = Q(is_image=True) & Q(name__in=custom_image_recipe_names) + return self.get_recipes().filter(criteria).order_by('name') + def get_outcome_text(self): return Build.BUILD_OUTCOME[int(self.outcome)][1] diff --git a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js index aa43284396..259271df33 100644 --- a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js +++ b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js @@ -76,7 +76,8 @@ function layerBtnsInit() { if (imgCustomModal.length == 0) throw("Modal new-custom-image not found"); - imgCustomModal.data('recipe', $(this).data('recipe')); + var recipe = {id: $(this).data('recipe'), name: null} + newCustomImageModalSetRecipes([recipe]); imgCustomModal.modal('show'); }); } diff --git a/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js b/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js index 98e87f4a6b..1ae0d34e90 100644 --- a/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +++ b/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js @@ -1,33 +1,59 @@ "use strict"; -/* Used for the newcustomimage_modal actions */ +/* +Used for the newcustomimage_modal actions + +The .data('recipe') value on the outer element determines which +recipe ID is used as the basis for the new custom image recipe created via +this modal. + +Use newCustomImageModalSetRecipes() to set the recipes available as a base +for the new custom image. This will manage the addition of radio buttons +to select the base image (or remove the radio buttons, if there is only a +single base image available). +*/ function newCustomImageModalInit(){ var newCustomImgBtn = $("#create-new-custom-image-btn"); var imgCustomModal = $("#new-custom-image-modal"); var invalidNameHelp = $("#invalid-name-help"); + var invalidRecipeHelp = $("#invalid-recipe-help"); var nameInput = imgCustomModal.find('input'); - var invalidMsg = "Image names cannot contain spaces or capital letters. The only allowed special character is dash (-)."; - var duplicateImageMsg = "An image with this name already exists in this project."; - var duplicateRecipeMsg = "A non-image recipe with this name already exists."; + var invalidNameMsg = "Image names cannot contain spaces or capital letters. The only allowed special character is dash (-)."; + var duplicateNameMsg = "An image with this name already exists. Image names must be unique."; + var invalidBaseRecipeIdMsg = "Please select an image to customise."; + + // capture clicks on radio buttons inside the modal; when one is selected, + // set the recipe on the modal + imgCustomModal.on("click", "[name='select-image']", function (e) { + clearRecipeError(); + + var recipeId = $(e.target).attr('data-recipe'); + imgCustomModal.data('recipe', recipeId); + }); newCustomImgBtn.click(function(e){ e.preventDefault(); var baseRecipeId = imgCustomModal.data('recipe'); + if (!baseRecipeId) { + showRecipeError(invalidBaseRecipeIdMsg); + return; + } + if (nameInput.val().length > 0) { libtoaster.createCustomRecipe(nameInput.val(), baseRecipeId, function(ret) { if (ret.error !== "ok") { console.warn(ret.error); if (ret.error === "invalid-name") { - showError(invalidMsg); - } else if (ret.error === "image-already-exists") { - showError(duplicateImageMsg); - } else if (ret.error === "recipe-already-exists") { - showError(duplicateRecipeMsg); + showNameError(invalidNameMsg); + return; + } else if (ret.error === "already-exists") { + showNameError(duplicateNameMsg); + return; } } else { imgCustomModal.modal('hide'); @@ -37,12 +63,21 @@ function newCustomImageModalInit(){ } }); - function showError(text){ + function showNameError(text){ invalidNameHelp.text(text); invalidNameHelp.show(); nameInput.parent().addClass('error'); } + function showRecipeError(text){ + invalidRecipeHelp.text(text); + invalidRecipeHelp.show(); + } + + function clearRecipeError(){ + invalidRecipeHelp.hide(); + } + nameInput.on('keyup', function(){ if (nameInput.val().length === 0){ newCustomImgBtn.prop("disabled", true); @@ -50,7 +85,7 @@ function newCustomImageModalInit(){ } if (nameInput.val().search(/[^a-z|0-9|-]/) != -1){ - showError(invalidMsg); + showNameError(invalidNameMsg); newCustomImgBtn.prop("disabled", true); nameInput.parent().addClass('error'); } else { @@ -60,3 +95,49 @@ function newCustomImageModalInit(){ } }); } + +// Set the image recipes which can used as the basis for the custom +// image recipe the user is creating +// +// baseRecipes: a list of one or more recipes which can be +// used as the base for the new custom image recipe in the format: +// [{'id': <recipe ID>, 'name': <recipe name>'}, ...] +// +// if recipes is a single recipe, just show the text box to set the +// name for the new custom image; if recipes contains multiple recipe objects, +// show a set of radio buttons so the user can decide which to use as the +// basis for the new custom image +function newCustomImageModalSetRecipes(baseRecipes) { + var imgCustomModal = $("#new-custom-image-modal"); + var imageSelector = $('#new-custom-image-modal [data-role="image-selector"]'); + var imageSelectRadiosContainer = $('#new-custom-image-modal [data-role="image-selector-radios"]'); + + if (baseRecipes.length === 1) { + // hide the radio button container + imageSelector.hide(); + + // remove any radio buttons + labels + imageSelector.remove('[data-role="image-radio"]'); + + // set the single recipe ID on the modal as it's the only one + // we can build from + imgCustomModal.data('recipe', baseRecipes[0].id); + } + else { + // add radio buttons; note that the handlers for the radio buttons + // are set in newCustomImageModalInit via event delegation + for (var i = 0; i < baseRecipes.length; i++) { + var recipe = baseRecipes[i]; + imageSelectRadiosContainer.append( + '<label class="radio" data-role="image-radio">' + + recipe.name + + '<input type="radio" class="form-control" name="select-image" ' + + 'data-recipe="' + recipe.id + '">' + + '</label>' + ); + } + + // show the radio button container + imageSelector.show(); + } +} diff --git a/bitbake/lib/toaster/toastergui/static/js/recipedetails.js b/bitbake/lib/toaster/toastergui/static/js/recipedetails.js index d5f9eacdce..604db5f037 100644 --- a/bitbake/lib/toaster/toastergui/static/js/recipedetails.js +++ b/bitbake/lib/toaster/toastergui/static/js/recipedetails.js @@ -9,7 +9,8 @@ function recipeDetailsPageInit(ctx){ if (imgCustomModal.length === 0) throw("Modal new-custom-image not found"); - imgCustomModal.data('recipe', $(this).data('recipe')); + var recipe = {id: $(this).data('recipe'), name: null} + newCustomImageModalSetRecipes([recipe]); imgCustomModal.modal('show'); }); diff --git a/bitbake/lib/toaster/toastergui/templates/basebuildpage.html b/bitbake/lib/toaster/toastergui/templates/basebuildpage.html index ff9433eee7..4a8e2a7abd 100644 --- a/bitbake/lib/toaster/toastergui/templates/basebuildpage.html +++ b/bitbake/lib/toaster/toastergui/templates/basebuildpage.html @@ -1,90 +1,149 @@ {% extends "base.html" %} {% load projecttags %} {% load project_url_tag %} +{% load queryset_to_list_filter %} {% load humanize %} {% block pagecontent %} + <!-- breadcrumbs --> + <div class="section"> + <ul class="breadcrumb" id="breadcrumb"> + <li><a href="{% project_url build.project %}">{{build.project.name}}</a></li> + {% if not build.project.is_default %} + <li><a href="{% url 'projectbuilds' build.project.id %}">Builds</a></li> + {% endif %} + <li> + {% block parentbreadcrumb %} + <a href="{%url 'builddashboard' build.pk%}"> + {{build.get_sorted_target_list.0.target}} {% if build.target_set.all.count > 1 %}(+{{build.target_set.all.count|add:"-1"}}){% endif %} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}}) + </a> + {% endblock %} + </li> + {% block localbreadcrumb %}{% endblock %} + </ul> + <script> + $( function () { + $('#breadcrumb > li').append('<span class="divider">→</span>'); + $('#breadcrumb > li:last').addClass("active"); + $('#breadcrumb > li:last > span').remove(); + }); + </script> + </div> + + <div class="row-fluid"> + <!-- begin left sidebar container --> + <div id="nav" class="span2"> + <ul class="nav nav-list well"> + <li + {% if request.resolver_match.url_name == 'builddashboard' %} + class="active" + {% endif %} > + <a class="nav-parent" href="{% url 'builddashboard' build.pk %}">Build summary</a> + </li> + {% if build.target_set.all.0.is_image and build.outcome == 0 %} + <li class="nav-header">Images</li> + {% block nav-target %} + {% for t in build.get_sorted_target_list %} + <li><a href="{% url 'target' build.pk t.pk %}">{{t.target}}</a><li> + {% endfor %} + {% endblock %} + {% endif %} + <li class="nav-header">Build</li> + {% block nav-configuration %} + <li><a href="{% url 'configuration' build.pk %}">Configuration</a></li> + {% endblock %} + {% block nav-tasks %} + <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li> + {% endblock %} + {% block nav-recipes %} + <li><a href="{% url 'recipes' build.pk %}">Recipes</a></li> + {% endblock %} + {% block nav-packages %} + <li><a href="{% url 'packages' build.pk %}">Packages</a></li> + {% endblock %} + <li class="nav-header">Performance</li> + {% block nav-buildtime %} + <li><a href="{% url 'buildtime' build.pk %}">Time</a></li> + {% endblock %} + {% block nav-cputime %} + <li><a href="{% url 'cputime' build.pk %}">CPU usage</a></li> + {% endblock %} + {% block nav-diskio %} + <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li> + {% endblock %} + <li class="divider"></li> - <div class=""> -<!-- Breadcrumbs --> - <div class="section"> - <ul class="breadcrumb" id="breadcrumb"> - <li><a href="{% project_url build.project %}">{{build.project.name}}</a></li> - {% if not build.project.is_default %} - <li><a href="{% url 'projectbuilds' build.project.id %}">Builds</a></li> - {% endif %} - <li> - {% block parentbreadcrumb %} - <a href="{%url 'builddashboard' build.pk%}"> - {{build.get_sorted_target_list.0.target}} {%if build.target_set.all.count > 1%}(+{{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}}) + <li> + <p class="navbar-btn"> + <a class="btn btn-block" href="{% url 'build_artifact' build.id 'cookerlog' build.id %}"> + Download build log </a> - {% endblock %} - </li> - {% block localbreadcrumb %}{% endblock %} - </ul> - <script> - $( function () { - $('#breadcrumb > li').append('<span class="divider">→</span>'); - $('#breadcrumb > li:last').addClass("active"); - $('#breadcrumb > li:last > span').remove(); - }); - </script> - </div> + </p> + </li> - <div class="row-fluid"> + <li> + <!-- edit custom image built during this build --> + <p class="navbar-btn" data-role="edit-custom-image-trigger"> + <button class="btn btn-block">Edit custom image</button> + </p> + {% include 'editcustomimage_modal.html' %} + <script> + $(document).ready(function () { + var editableCustomImageRecipes = {{ build.get_custom_image_recipes | queryset_to_list:"id,name" | json }}; - <!-- begin left sidebar container --> - <div id="nav" class="span2"> - <ul class="nav nav-list well"> - <li - {% if request.resolver_match.url_name == 'builddashboard' %} - class="active" - {% endif %} > - <a class="nav-parent" href="{% url 'builddashboard' build.pk %}">Build summary</a> - </li> - {% if build.target_set.all.0.is_image and build.outcome == 0 %} - <li class="nav-header">Images</li> - {% block nav-target %} - {% for t in build.get_sorted_target_list %} - <li><a href="{% url 'target' build.pk t.pk %}">{{t.target}}</a><li> - {% endfor %} - {% endblock %} - {% endif %} - <li class="nav-header">Build</li> - {% block nav-configuration %} - <li><a href="{% url 'configuration' build.pk %}">Configuration</a></li> - {% endblock %} - {% block nav-tasks %} - <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li> - {% endblock %} - {% block nav-recipes %} - <li><a href="{% url 'recipes' build.pk %}">Recipes</a></li> - {% endblock %} - {% block nav-packages %} - <li><a href="{% url 'packages' build.pk %}">Packages</a></li> - {% endblock %} - <li class="nav-header">Performance</li> - {% block nav-buildtime %} - <li><a href="{% url 'buildtime' build.pk %}">Time</a></li> - {% endblock %} - {% block nav-cputime %} - <li><a href="{% url 'cputime' build.pk %}">CPU time</a></li> - {% endblock %} - {% block nav-diskio %} - <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li> - {% endblock %} - </ul> - </div> - <!-- end left sidebar container --> + // edit custom image which was built during this build + var editCustomImageModal = $('#edit-custom-image-modal'); + var editCustomImageTrigger = $('[data-role="edit-custom-image-trigger"]'); - <!-- Begin right container --> - {% block buildinfomain %}{% endblock %} - <!-- End right container --> + editCustomImageTrigger.click(function () { + // if there is a single editable custom image, go direct to the edit + // page for it; if there are multiple editable custom images, show + // dialog to select one of them for editing + // single editable custom image - </div> - </div> + // multiple editable custom images + editCustomImageModal.modal('show'); + }); + }); + </script> + </li> + <li> + <!-- new custom image from image recipe in this build --> + <p class="navbar-btn" data-role="new-custom-image-trigger"> + <button class="btn btn-block">New custom image</button> + </p> + {% include 'newcustomimage_modal.html' %} + <script> + // imageRecipes includes both custom image recipes and built-in + // image recipes, any of which can be used as the basis for a + // new custom image + var imageRecipes = {{ build.get_image_recipes | queryset_to_list:"id,name" | json }}; -{% endblock %} + $(document).ready(function () { + var newCustomImageModal = $('#new-custom-image-modal'); + var newCustomImageTrigger = $('[data-role="new-custom-image-trigger"]'); + // show create new custom image modal to select an image built + // during this build as the basis for the custom recipe + newCustomImageTrigger.click(function () { + if (!imageRecipes.length) { + return; + } + newCustomImageModalSetRecipes(imageRecipes); + newCustomImageModal.modal('show'); + }); + }); + </script> + </li> + </ul> + + </div> + <!-- end left sidebar container --> + + <!-- begin right container --> + {% block buildinfomain %}{% endblock %} + <!-- end right container --> + </div> +{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html b/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html new file mode 100644 index 0000000000..fd998f63eb --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html @@ -0,0 +1,23 @@ +<!-- +modal dialog shown on the build dashboard, for editing an existing custom image +--> +<div class="modal hide fade in" aria-hidden="false" id="edit-custom-image-modal"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Select custom image to edit</h3> + </div> + <div class="modal-body"> + <div class="row-fluid"> + <span class="help-block"> + Explanation of what this modal is for + </span> + </div> + <div class="control-group controls"> + <input type="text" class="huge" placeholder="input box" required> + <span class="help-block error" style="display:none">Error text</span> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-primary btn-large" disabled>Action</button> + </div> +</div> diff --git a/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html b/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html index b1b5148c08..caeb302352 100644 --- a/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html +++ b/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html @@ -15,18 +15,34 @@ <div class="modal hide fade in" id="new-custom-image-modal" aria-hidden="false"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Name your custom image</h3> + <h3>New custom image</h3> </div> + <div class="modal-body"> + <!-- + this container is visible if there are multiple image recipes which could + be used as a basis for the new custom image; radio buttons are added to it + via newCustomImageModalSetRecipes() as required + --> + <div data-role="image-selector" style="display:none;"> + <h4>Which image do you want to customise?</h4> + <div data-role="image-selector-radios"></div> + <span class="help-block error" id="invalid-recipe-help" style="display:none"></span> + <div class="air"></div> + </div> + + <h4>Name your custom image</h4> + <div class="row-fluid"> <span class="help-block span8">Image names must be unique. They should not contain spaces or capital letters, and the only allowed special character is dash (-).<p></p> </span></div> <div class="control-group controls"> <input type="text" class="huge" placeholder="Type the custom image name" required> - <span class="help-block error" id="invalid-name-help" style="display:none"></span> - </div> - </div> - <div class="modal-footer"> - <button id="create-new-custom-image-btn" class="btn btn-primary btn-large" data-original-title="" title="" disabled>Create custom image</button> + <span class="help-block error" id="invalid-name-help" style="display:none"></span> </div> + </div> + + <div class="modal-footer"> + <button id="create-new-custom-image-btn" class="btn btn-primary btn-large" data-original-title="" title="" disabled>Create custom image</button> + </div> </div> diff --git a/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py b/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py new file mode 100644 index 0000000000..dfc094b591 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py @@ -0,0 +1,26 @@ +from django import template +import json + +register = template.Library() + +def queryset_to_list(queryset, fields): + """ + Convert a queryset to a list; fields can be set to a comma-separated + string of fields for each record included in the resulting list; if + omitted, all fields are included for each record, e.g. + + {{ queryset | queryset_to_list:"id,name" }} + + will return a list like + + [{'id': 1, 'name': 'foo'}, ...] + + (providing queryset has id and name fields) + """ + if fields: + fields_list = [field.strip() for field in fields.split(',')] + return list(queryset.values(*fields_list)) + else: + return list(queryset.values()) + +register.filter('queryset_to_list', queryset_to_list) diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py index 9744f4efaf..942dc31ae9 100755 --- a/bitbake/lib/toaster/toastergui/views.py +++ b/bitbake/lib/toaster/toastergui/views.py @@ -1257,7 +1257,10 @@ def recipes(request, build_id): if retval: return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id) (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe) - queryset = Recipe.objects.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id)).select_related("layer_version", "layer_version__layer") + + build = Build.objects.get(pk=build_id) + + queryset = build.get_recipes() queryset = _get_queryset(Recipe, queryset, filter_string, search_term, ordering_string, 'name') recipes = _build_page_range(Paginator(queryset, pagesize),request.GET.get('page', 1)) @@ -1276,8 +1279,6 @@ def recipes(request, build_id): revlist.append(recipe_dep) revs[recipe.id] = revlist - build = Build.objects.get(pk=build_id) - context = { 'objectname': 'recipes', 'build': build, |