diff options
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): | |||
| 490 | tgts = Target.objects.filter(build_id = self.id).order_by( 'target' ); | 490 | tgts = Target.objects.filter(build_id = self.id).order_by( 'target' ); |
| 491 | return( tgts ); | 491 | return( tgts ); |
| 492 | 492 | ||
| 493 | def get_recipes(self): | ||
| 494 | """ | ||
| 495 | Get the recipes related to this build; | ||
| 496 | note that the related layer versions and layers are also prefetched | ||
| 497 | by this query, as this queryset can be sorted by these objects in the | ||
| 498 | build recipes view; prefetching them here removes the need | ||
| 499 | for another query in that view | ||
| 500 | """ | ||
| 501 | layer_versions = Layer_Version.objects.filter(build=self) | ||
| 502 | criteria = Q(layer_version__id__in=layer_versions) | ||
| 503 | return Recipe.objects.filter(criteria) \ | ||
| 504 | .select_related('layer_version', 'layer_version__layer') | ||
| 505 | |||
| 506 | def get_custom_image_recipe_names(self): | ||
| 507 | """ | ||
| 508 | Get the names of custom image recipes for this build's project | ||
| 509 | as a list; this is used to screen out custom image recipes from the | ||
| 510 | recipes for the build by name, and to distinguish image recipes from | ||
| 511 | custom image recipes | ||
| 512 | """ | ||
| 513 | custom_image_recipes = \ | ||
| 514 | CustomImageRecipe.objects.filter(project=self.project) | ||
| 515 | return custom_image_recipes.values_list('name', flat=True) | ||
| 516 | |||
| 517 | def get_image_recipes(self): | ||
| 518 | """ | ||
| 519 | Returns a queryset of image recipes related to this build, sorted | ||
| 520 | by name | ||
| 521 | """ | ||
| 522 | criteria = Q(is_image=True) | ||
| 523 | return self.get_recipes().filter(criteria).order_by('name') | ||
| 524 | |||
| 525 | def get_custom_image_recipes(self): | ||
| 526 | """ | ||
| 527 | Returns a queryset of custom image recipes related to this build, | ||
| 528 | sorted by name | ||
| 529 | """ | ||
| 530 | custom_image_recipe_names = self.get_custom_image_recipe_names() | ||
| 531 | criteria = Q(is_image=True) & Q(name__in=custom_image_recipe_names) | ||
| 532 | return self.get_recipes().filter(criteria).order_by('name') | ||
| 533 | |||
| 493 | def get_outcome_text(self): | 534 | def get_outcome_text(self): |
| 494 | return Build.BUILD_OUTCOME[int(self.outcome)][1] | 535 | return Build.BUILD_OUTCOME[int(self.outcome)][1] |
| 495 | 536 | ||
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() { | |||
| 76 | if (imgCustomModal.length == 0) | 76 | if (imgCustomModal.length == 0) |
| 77 | throw("Modal new-custom-image not found"); | 77 | throw("Modal new-custom-image not found"); |
| 78 | 78 | ||
| 79 | imgCustomModal.data('recipe', $(this).data('recipe')); | 79 | var recipe = {id: $(this).data('recipe'), name: null} |
| 80 | newCustomImageModalSetRecipes([recipe]); | ||
| 80 | imgCustomModal.modal('show'); | 81 | imgCustomModal.modal('show'); |
| 81 | }); | 82 | }); |
| 82 | } | 83 | } |
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 @@ | |||
| 1 | "use strict"; | 1 | "use strict"; |
| 2 | 2 | ||
| 3 | /* Used for the newcustomimage_modal actions */ | 3 | /* |
| 4 | Used for the newcustomimage_modal actions | ||
| 5 | |||
| 6 | The .data('recipe') value on the outer element determines which | ||
| 7 | recipe ID is used as the basis for the new custom image recipe created via | ||
| 8 | this modal. | ||
| 9 | |||
| 10 | Use newCustomImageModalSetRecipes() to set the recipes available as a base | ||
| 11 | for the new custom image. This will manage the addition of radio buttons | ||
| 12 | to select the base image (or remove the radio buttons, if there is only a | ||
| 13 | single base image available). | ||
| 14 | */ | ||
| 4 | function newCustomImageModalInit(){ | 15 | function newCustomImageModalInit(){ |
| 5 | 16 | ||
| 6 | var newCustomImgBtn = $("#create-new-custom-image-btn"); | 17 | var newCustomImgBtn = $("#create-new-custom-image-btn"); |
| 7 | var imgCustomModal = $("#new-custom-image-modal"); | 18 | var imgCustomModal = $("#new-custom-image-modal"); |
| 8 | var invalidNameHelp = $("#invalid-name-help"); | 19 | var invalidNameHelp = $("#invalid-name-help"); |
| 20 | var invalidRecipeHelp = $("#invalid-recipe-help"); | ||
| 9 | var nameInput = imgCustomModal.find('input'); | 21 | var nameInput = imgCustomModal.find('input'); |
| 10 | 22 | ||
| 11 | var invalidMsg = "Image names cannot contain spaces or capital letters. The only allowed special character is dash (-)."; | 23 | var invalidNameMsg = "Image names cannot contain spaces or capital letters. The only allowed special character is dash (-)."; |
| 12 | var duplicateImageMsg = "An image with this name already exists in this project."; | 24 | var duplicateNameMsg = "An image with this name already exists. Image names must be unique."; |
| 13 | var duplicateRecipeMsg = "A non-image recipe with this name already exists."; | 25 | var invalidBaseRecipeIdMsg = "Please select an image to customise."; |
| 26 | |||
| 27 | // capture clicks on radio buttons inside the modal; when one is selected, | ||
| 28 | // set the recipe on the modal | ||
| 29 | imgCustomModal.on("click", "[name='select-image']", function (e) { | ||
| 30 | clearRecipeError(); | ||
| 31 | |||
| 32 | var recipeId = $(e.target).attr('data-recipe'); | ||
| 33 | imgCustomModal.data('recipe', recipeId); | ||
| 34 | }); | ||
| 14 | 35 | ||
| 15 | newCustomImgBtn.click(function(e){ | 36 | newCustomImgBtn.click(function(e){ |
| 16 | e.preventDefault(); | 37 | e.preventDefault(); |
| 17 | 38 | ||
| 18 | var baseRecipeId = imgCustomModal.data('recipe'); | 39 | var baseRecipeId = imgCustomModal.data('recipe'); |
| 19 | 40 | ||
| 41 | if (!baseRecipeId) { | ||
| 42 | showRecipeError(invalidBaseRecipeIdMsg); | ||
| 43 | return; | ||
| 44 | } | ||
| 45 | |||
| 20 | if (nameInput.val().length > 0) { | 46 | if (nameInput.val().length > 0) { |
| 21 | libtoaster.createCustomRecipe(nameInput.val(), baseRecipeId, | 47 | libtoaster.createCustomRecipe(nameInput.val(), baseRecipeId, |
| 22 | function(ret) { | 48 | function(ret) { |
| 23 | if (ret.error !== "ok") { | 49 | if (ret.error !== "ok") { |
| 24 | console.warn(ret.error); | 50 | console.warn(ret.error); |
| 25 | if (ret.error === "invalid-name") { | 51 | if (ret.error === "invalid-name") { |
| 26 | showError(invalidMsg); | 52 | showNameError(invalidNameMsg); |
| 27 | } else if (ret.error === "image-already-exists") { | 53 | return; |
| 28 | showError(duplicateImageMsg); | 54 | } else if (ret.error === "already-exists") { |
| 29 | } else if (ret.error === "recipe-already-exists") { | 55 | showNameError(duplicateNameMsg); |
| 30 | showError(duplicateRecipeMsg); | 56 | return; |
| 31 | } | 57 | } |
| 32 | } else { | 58 | } else { |
| 33 | imgCustomModal.modal('hide'); | 59 | imgCustomModal.modal('hide'); |
| @@ -37,12 +63,21 @@ function newCustomImageModalInit(){ | |||
| 37 | } | 63 | } |
| 38 | }); | 64 | }); |
| 39 | 65 | ||
| 40 | function showError(text){ | 66 | function showNameError(text){ |
| 41 | invalidNameHelp.text(text); | 67 | invalidNameHelp.text(text); |
| 42 | invalidNameHelp.show(); | 68 | invalidNameHelp.show(); |
| 43 | nameInput.parent().addClass('error'); | 69 | nameInput.parent().addClass('error'); |
| 44 | } | 70 | } |
| 45 | 71 | ||
| 72 | function showRecipeError(text){ | ||
| 73 | invalidRecipeHelp.text(text); | ||
| 74 | invalidRecipeHelp.show(); | ||
| 75 | } | ||
| 76 | |||
| 77 | function clearRecipeError(){ | ||
| 78 | invalidRecipeHelp.hide(); | ||
| 79 | } | ||
| 80 | |||
| 46 | nameInput.on('keyup', function(){ | 81 | nameInput.on('keyup', function(){ |
| 47 | if (nameInput.val().length === 0){ | 82 | if (nameInput.val().length === 0){ |
| 48 | newCustomImgBtn.prop("disabled", true); | 83 | newCustomImgBtn.prop("disabled", true); |
| @@ -50,7 +85,7 @@ function newCustomImageModalInit(){ | |||
| 50 | } | 85 | } |
| 51 | 86 | ||
| 52 | if (nameInput.val().search(/[^a-z|0-9|-]/) != -1){ | 87 | if (nameInput.val().search(/[^a-z|0-9|-]/) != -1){ |
| 53 | showError(invalidMsg); | 88 | showNameError(invalidNameMsg); |
| 54 | newCustomImgBtn.prop("disabled", true); | 89 | newCustomImgBtn.prop("disabled", true); |
| 55 | nameInput.parent().addClass('error'); | 90 | nameInput.parent().addClass('error'); |
| 56 | } else { | 91 | } else { |
| @@ -60,3 +95,49 @@ function newCustomImageModalInit(){ | |||
| 60 | } | 95 | } |
| 61 | }); | 96 | }); |
| 62 | } | 97 | } |
| 98 | |||
| 99 | // Set the image recipes which can used as the basis for the custom | ||
| 100 | // image recipe the user is creating | ||
| 101 | // | ||
| 102 | // baseRecipes: a list of one or more recipes which can be | ||
| 103 | // used as the base for the new custom image recipe in the format: | ||
| 104 | // [{'id': <recipe ID>, 'name': <recipe name>'}, ...] | ||
| 105 | // | ||
| 106 | // if recipes is a single recipe, just show the text box to set the | ||
| 107 | // name for the new custom image; if recipes contains multiple recipe objects, | ||
| 108 | // show a set of radio buttons so the user can decide which to use as the | ||
| 109 | // basis for the new custom image | ||
| 110 | function newCustomImageModalSetRecipes(baseRecipes) { | ||
| 111 | var imgCustomModal = $("#new-custom-image-modal"); | ||
| 112 | var imageSelector = $('#new-custom-image-modal [data-role="image-selector"]'); | ||
| 113 | var imageSelectRadiosContainer = $('#new-custom-image-modal [data-role="image-selector-radios"]'); | ||
| 114 | |||
| 115 | if (baseRecipes.length === 1) { | ||
| 116 | // hide the radio button container | ||
| 117 | imageSelector.hide(); | ||
| 118 | |||
| 119 | // remove any radio buttons + labels | ||
| 120 | imageSelector.remove('[data-role="image-radio"]'); | ||
| 121 | |||
| 122 | // set the single recipe ID on the modal as it's the only one | ||
| 123 | // we can build from | ||
| 124 | imgCustomModal.data('recipe', baseRecipes[0].id); | ||
| 125 | } | ||
| 126 | else { | ||
| 127 | // add radio buttons; note that the handlers for the radio buttons | ||
| 128 | // are set in newCustomImageModalInit via event delegation | ||
| 129 | for (var i = 0; i < baseRecipes.length; i++) { | ||
| 130 | var recipe = baseRecipes[i]; | ||
| 131 | imageSelectRadiosContainer.append( | ||
| 132 | '<label class="radio" data-role="image-radio">' + | ||
| 133 | recipe.name + | ||
| 134 | '<input type="radio" class="form-control" name="select-image" ' + | ||
| 135 | 'data-recipe="' + recipe.id + '">' + | ||
| 136 | '</label>' | ||
| 137 | ); | ||
| 138 | } | ||
| 139 | |||
| 140 | // show the radio button container | ||
| 141 | imageSelector.show(); | ||
| 142 | } | ||
| 143 | } | ||
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){ | |||
| 9 | if (imgCustomModal.length === 0) | 9 | if (imgCustomModal.length === 0) |
| 10 | throw("Modal new-custom-image not found"); | 10 | throw("Modal new-custom-image not found"); |
| 11 | 11 | ||
| 12 | imgCustomModal.data('recipe', $(this).data('recipe')); | 12 | var recipe = {id: $(this).data('recipe'), name: null} |
| 13 | newCustomImageModalSetRecipes([recipe]); | ||
| 13 | imgCustomModal.modal('show'); | 14 | imgCustomModal.modal('show'); |
| 14 | }); | 15 | }); |
| 15 | 16 | ||
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 @@ | |||
| 1 | {% extends "base.html" %} | 1 | {% extends "base.html" %} |
| 2 | {% load projecttags %} | 2 | {% load projecttags %} |
| 3 | {% load project_url_tag %} | 3 | {% load project_url_tag %} |
| 4 | {% load queryset_to_list_filter %} | ||
| 4 | {% load humanize %} | 5 | {% load humanize %} |
| 5 | {% block pagecontent %} | 6 | {% block pagecontent %} |
| 7 | <!-- breadcrumbs --> | ||
| 8 | <div class="section"> | ||
| 9 | <ul class="breadcrumb" id="breadcrumb"> | ||
| 10 | <li><a href="{% project_url build.project %}">{{build.project.name}}</a></li> | ||
| 11 | {% if not build.project.is_default %} | ||
| 12 | <li><a href="{% url 'projectbuilds' build.project.id %}">Builds</a></li> | ||
| 13 | {% endif %} | ||
| 14 | <li> | ||
| 15 | {% block parentbreadcrumb %} | ||
| 16 | <a href="{%url 'builddashboard' build.pk%}"> | ||
| 17 | {{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"}}) | ||
| 18 | </a> | ||
| 19 | {% endblock %} | ||
| 20 | </li> | ||
| 21 | {% block localbreadcrumb %}{% endblock %} | ||
| 22 | </ul> | ||
| 23 | <script> | ||
| 24 | $( function () { | ||
| 25 | $('#breadcrumb > li').append('<span class="divider">→</span>'); | ||
| 26 | $('#breadcrumb > li:last').addClass("active"); | ||
| 27 | $('#breadcrumb > li:last > span').remove(); | ||
| 28 | }); | ||
| 29 | </script> | ||
| 30 | </div> | ||
| 31 | |||
| 32 | <div class="row-fluid"> | ||
| 33 | <!-- begin left sidebar container --> | ||
| 34 | <div id="nav" class="span2"> | ||
| 35 | <ul class="nav nav-list well"> | ||
| 36 | <li | ||
| 37 | {% if request.resolver_match.url_name == 'builddashboard' %} | ||
| 38 | class="active" | ||
| 39 | {% endif %} > | ||
| 40 | <a class="nav-parent" href="{% url 'builddashboard' build.pk %}">Build summary</a> | ||
| 41 | </li> | ||
| 42 | {% if build.target_set.all.0.is_image and build.outcome == 0 %} | ||
| 43 | <li class="nav-header">Images</li> | ||
| 44 | {% block nav-target %} | ||
| 45 | {% for t in build.get_sorted_target_list %} | ||
| 46 | <li><a href="{% url 'target' build.pk t.pk %}">{{t.target}}</a><li> | ||
| 47 | {% endfor %} | ||
| 48 | {% endblock %} | ||
| 49 | {% endif %} | ||
| 50 | <li class="nav-header">Build</li> | ||
| 51 | {% block nav-configuration %} | ||
| 52 | <li><a href="{% url 'configuration' build.pk %}">Configuration</a></li> | ||
| 53 | {% endblock %} | ||
| 54 | {% block nav-tasks %} | ||
| 55 | <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li> | ||
| 56 | {% endblock %} | ||
| 57 | {% block nav-recipes %} | ||
| 58 | <li><a href="{% url 'recipes' build.pk %}">Recipes</a></li> | ||
| 59 | {% endblock %} | ||
| 60 | {% block nav-packages %} | ||
| 61 | <li><a href="{% url 'packages' build.pk %}">Packages</a></li> | ||
| 62 | {% endblock %} | ||
| 63 | <li class="nav-header">Performance</li> | ||
| 64 | {% block nav-buildtime %} | ||
| 65 | <li><a href="{% url 'buildtime' build.pk %}">Time</a></li> | ||
| 66 | {% endblock %} | ||
| 67 | {% block nav-cputime %} | ||
| 68 | <li><a href="{% url 'cputime' build.pk %}">CPU usage</a></li> | ||
| 69 | {% endblock %} | ||
| 70 | {% block nav-diskio %} | ||
| 71 | <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li> | ||
| 72 | {% endblock %} | ||
| 6 | 73 | ||
| 74 | <li class="divider"></li> | ||
| 7 | 75 | ||
| 8 | <div class=""> | 76 | <li> |
| 9 | <!-- Breadcrumbs --> | 77 | <p class="navbar-btn"> |
| 10 | <div class="section"> | 78 | <a class="btn btn-block" href="{% url 'build_artifact' build.id 'cookerlog' build.id %}"> |
| 11 | <ul class="breadcrumb" id="breadcrumb"> | 79 | Download build log |
| 12 | <li><a href="{% project_url build.project %}">{{build.project.name}}</a></li> | ||
| 13 | {% if not build.project.is_default %} | ||
| 14 | <li><a href="{% url 'projectbuilds' build.project.id %}">Builds</a></li> | ||
| 15 | {% endif %} | ||
| 16 | <li> | ||
| 17 | {% block parentbreadcrumb %} | ||
| 18 | <a href="{%url 'builddashboard' build.pk%}"> | ||
| 19 | {{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"}}) | ||
| 20 | </a> | 80 | </a> |
| 21 | {% endblock %} | 81 | </p> |
| 22 | </li> | 82 | </li> |
| 23 | {% block localbreadcrumb %}{% endblock %} | ||
| 24 | </ul> | ||
| 25 | <script> | ||
| 26 | $( function () { | ||
| 27 | $('#breadcrumb > li').append('<span class="divider">→</span>'); | ||
| 28 | $('#breadcrumb > li:last').addClass("active"); | ||
| 29 | $('#breadcrumb > li:last > span').remove(); | ||
| 30 | }); | ||
| 31 | </script> | ||
| 32 | </div> | ||
| 33 | 83 | ||
| 34 | <div class="row-fluid"> | 84 | <li> |
| 85 | <!-- edit custom image built during this build --> | ||
| 86 | <p class="navbar-btn" data-role="edit-custom-image-trigger"> | ||
| 87 | <button class="btn btn-block">Edit custom image</button> | ||
| 88 | </p> | ||
| 89 | {% include 'editcustomimage_modal.html' %} | ||
| 90 | <script> | ||
| 91 | $(document).ready(function () { | ||
| 92 | var editableCustomImageRecipes = {{ build.get_custom_image_recipes | queryset_to_list:"id,name" | json }}; | ||
| 35 | 93 | ||
| 36 | <!-- begin left sidebar container --> | 94 | // edit custom image which was built during this build |
| 37 | <div id="nav" class="span2"> | 95 | var editCustomImageModal = $('#edit-custom-image-modal'); |
| 38 | <ul class="nav nav-list well"> | 96 | var editCustomImageTrigger = $('[data-role="edit-custom-image-trigger"]'); |
| 39 | <li | ||
| 40 | {% if request.resolver_match.url_name == 'builddashboard' %} | ||
| 41 | class="active" | ||
| 42 | {% endif %} > | ||
| 43 | <a class="nav-parent" href="{% url 'builddashboard' build.pk %}">Build summary</a> | ||
| 44 | </li> | ||
| 45 | {% if build.target_set.all.0.is_image and build.outcome == 0 %} | ||
| 46 | <li class="nav-header">Images</li> | ||
| 47 | {% block nav-target %} | ||
| 48 | {% for t in build.get_sorted_target_list %} | ||
| 49 | <li><a href="{% url 'target' build.pk t.pk %}">{{t.target}}</a><li> | ||
| 50 | {% endfor %} | ||
| 51 | {% endblock %} | ||
| 52 | {% endif %} | ||
| 53 | <li class="nav-header">Build</li> | ||
| 54 | {% block nav-configuration %} | ||
| 55 | <li><a href="{% url 'configuration' build.pk %}">Configuration</a></li> | ||
| 56 | {% endblock %} | ||
| 57 | {% block nav-tasks %} | ||
| 58 | <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li> | ||
| 59 | {% endblock %} | ||
| 60 | {% block nav-recipes %} | ||
| 61 | <li><a href="{% url 'recipes' build.pk %}">Recipes</a></li> | ||
| 62 | {% endblock %} | ||
| 63 | {% block nav-packages %} | ||
| 64 | <li><a href="{% url 'packages' build.pk %}">Packages</a></li> | ||
| 65 | {% endblock %} | ||
| 66 | <li class="nav-header">Performance</li> | ||
| 67 | {% block nav-buildtime %} | ||
| 68 | <li><a href="{% url 'buildtime' build.pk %}">Time</a></li> | ||
| 69 | {% endblock %} | ||
| 70 | {% block nav-cputime %} | ||
| 71 | <li><a href="{% url 'cputime' build.pk %}">CPU time</a></li> | ||
| 72 | {% endblock %} | ||
| 73 | {% block nav-diskio %} | ||
| 74 | <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li> | ||
| 75 | {% endblock %} | ||
| 76 | </ul> | ||
| 77 | </div> | ||
| 78 | <!-- end left sidebar container --> | ||
| 79 | 97 | ||
| 80 | <!-- Begin right container --> | 98 | editCustomImageTrigger.click(function () { |
| 81 | {% block buildinfomain %}{% endblock %} | 99 | // if there is a single editable custom image, go direct to the edit |
| 82 | <!-- End right container --> | 100 | // page for it; if there are multiple editable custom images, show |
| 101 | // dialog to select one of them for editing | ||
| 83 | 102 | ||
| 103 | // single editable custom image | ||
| 84 | 104 | ||
| 85 | </div> | 105 | // multiple editable custom images |
| 86 | </div> | 106 | editCustomImageModal.modal('show'); |
| 107 | }); | ||
| 108 | }); | ||
| 109 | </script> | ||
| 110 | </li> | ||
| 87 | 111 | ||
| 112 | <li> | ||
| 113 | <!-- new custom image from image recipe in this build --> | ||
| 114 | <p class="navbar-btn" data-role="new-custom-image-trigger"> | ||
| 115 | <button class="btn btn-block">New custom image</button> | ||
| 116 | </p> | ||
| 117 | {% include 'newcustomimage_modal.html' %} | ||
| 118 | <script> | ||
| 119 | // imageRecipes includes both custom image recipes and built-in | ||
| 120 | // image recipes, any of which can be used as the basis for a | ||
| 121 | // new custom image | ||
| 122 | var imageRecipes = {{ build.get_image_recipes | queryset_to_list:"id,name" | json }}; | ||
| 88 | 123 | ||
| 89 | {% endblock %} | 124 | $(document).ready(function () { |
| 125 | var newCustomImageModal = $('#new-custom-image-modal'); | ||
| 126 | var newCustomImageTrigger = $('[data-role="new-custom-image-trigger"]'); | ||
| 90 | 127 | ||
| 128 | // show create new custom image modal to select an image built | ||
| 129 | // during this build as the basis for the custom recipe | ||
| 130 | newCustomImageTrigger.click(function () { | ||
| 131 | if (!imageRecipes.length) { | ||
| 132 | return; | ||
| 133 | } | ||
| 134 | newCustomImageModalSetRecipes(imageRecipes); | ||
| 135 | newCustomImageModal.modal('show'); | ||
| 136 | }); | ||
| 137 | }); | ||
| 138 | </script> | ||
| 139 | </li> | ||
| 140 | </ul> | ||
| 141 | |||
| 142 | </div> | ||
| 143 | <!-- end left sidebar container --> | ||
| 144 | |||
| 145 | <!-- begin right container --> | ||
| 146 | {% block buildinfomain %}{% endblock %} | ||
| 147 | <!-- end right container --> | ||
| 148 | </div> | ||
| 149 | {% 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 @@ | |||
| 1 | <!-- | ||
| 2 | modal dialog shown on the build dashboard, for editing an existing custom image | ||
| 3 | --> | ||
| 4 | <div class="modal hide fade in" aria-hidden="false" id="edit-custom-image-modal"> | ||
| 5 | <div class="modal-header"> | ||
| 6 | <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> | ||
| 7 | <h3>Select custom image to edit</h3> | ||
| 8 | </div> | ||
| 9 | <div class="modal-body"> | ||
| 10 | <div class="row-fluid"> | ||
| 11 | <span class="help-block"> | ||
| 12 | Explanation of what this modal is for | ||
| 13 | </span> | ||
| 14 | </div> | ||
| 15 | <div class="control-group controls"> | ||
| 16 | <input type="text" class="huge" placeholder="input box" required> | ||
| 17 | <span class="help-block error" style="display:none">Error text</span> | ||
| 18 | </div> | ||
| 19 | </div> | ||
| 20 | <div class="modal-footer"> | ||
| 21 | <button class="btn btn-primary btn-large" disabled>Action</button> | ||
| 22 | </div> | ||
| 23 | </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 @@ | |||
| 15 | <div class="modal hide fade in" id="new-custom-image-modal" aria-hidden="false"> | 15 | <div class="modal hide fade in" id="new-custom-image-modal" aria-hidden="false"> |
| 16 | <div class="modal-header"> | 16 | <div class="modal-header"> |
| 17 | <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> | 17 | <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
| 18 | <h3>Name your custom image</h3> | 18 | <h3>New custom image</h3> |
| 19 | </div> | 19 | </div> |
| 20 | |||
| 20 | <div class="modal-body"> | 21 | <div class="modal-body"> |
| 22 | <!-- | ||
| 23 | this container is visible if there are multiple image recipes which could | ||
| 24 | be used as a basis for the new custom image; radio buttons are added to it | ||
| 25 | via newCustomImageModalSetRecipes() as required | ||
| 26 | --> | ||
| 27 | <div data-role="image-selector" style="display:none;"> | ||
| 28 | <h4>Which image do you want to customise?</h4> | ||
| 29 | <div data-role="image-selector-radios"></div> | ||
| 30 | <span class="help-block error" id="invalid-recipe-help" style="display:none"></span> | ||
| 31 | <div class="air"></div> | ||
| 32 | </div> | ||
| 33 | |||
| 34 | <h4>Name your custom image</h4> | ||
| 35 | |||
| 21 | <div class="row-fluid"> | 36 | <div class="row-fluid"> |
| 22 | <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> | 37 | <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> |
| 23 | </span></div> | 38 | </span></div> |
| 24 | <div class="control-group controls"> | 39 | <div class="control-group controls"> |
| 25 | <input type="text" class="huge" placeholder="Type the custom image name" required> | 40 | <input type="text" class="huge" placeholder="Type the custom image name" required> |
| 26 | <span class="help-block error" id="invalid-name-help" style="display:none"></span> | 41 | <span class="help-block error" id="invalid-name-help" style="display:none"></span> |
| 27 | </div> | ||
| 28 | </div> | ||
| 29 | <div class="modal-footer"> | ||
| 30 | <button id="create-new-custom-image-btn" class="btn btn-primary btn-large" data-original-title="" title="" disabled>Create custom image</button> | ||
| 31 | </div> | 42 | </div> |
| 43 | </div> | ||
| 44 | |||
| 45 | <div class="modal-footer"> | ||
| 46 | <button id="create-new-custom-image-btn" class="btn btn-primary btn-large" data-original-title="" title="" disabled>Create custom image</button> | ||
| 47 | </div> | ||
| 32 | </div> | 48 | </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 @@ | |||
| 1 | from django import template | ||
| 2 | import json | ||
| 3 | |||
| 4 | register = template.Library() | ||
| 5 | |||
| 6 | def queryset_to_list(queryset, fields): | ||
| 7 | """ | ||
| 8 | Convert a queryset to a list; fields can be set to a comma-separated | ||
| 9 | string of fields for each record included in the resulting list; if | ||
| 10 | omitted, all fields are included for each record, e.g. | ||
| 11 | |||
| 12 | {{ queryset | queryset_to_list:"id,name" }} | ||
| 13 | |||
| 14 | will return a list like | ||
| 15 | |||
| 16 | [{'id': 1, 'name': 'foo'}, ...] | ||
| 17 | |||
| 18 | (providing queryset has id and name fields) | ||
| 19 | """ | ||
| 20 | if fields: | ||
| 21 | fields_list = [field.strip() for field in fields.split(',')] | ||
| 22 | return list(queryset.values(*fields_list)) | ||
| 23 | else: | ||
| 24 | return list(queryset.values()) | ||
| 25 | |||
| 26 | 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): | |||
| 1257 | if retval: | 1257 | if retval: |
| 1258 | return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id) | 1258 | return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id) |
| 1259 | (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe) | 1259 | (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe) |
| 1260 | queryset = Recipe.objects.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id)).select_related("layer_version", "layer_version__layer") | 1260 | |
| 1261 | build = Build.objects.get(pk=build_id) | ||
| 1262 | |||
| 1263 | queryset = build.get_recipes() | ||
| 1261 | queryset = _get_queryset(Recipe, queryset, filter_string, search_term, ordering_string, 'name') | 1264 | queryset = _get_queryset(Recipe, queryset, filter_string, search_term, ordering_string, 'name') |
| 1262 | 1265 | ||
| 1263 | recipes = _build_page_range(Paginator(queryset, pagesize),request.GET.get('page', 1)) | 1266 | recipes = _build_page_range(Paginator(queryset, pagesize),request.GET.get('page', 1)) |
| @@ -1276,8 +1279,6 @@ def recipes(request, build_id): | |||
| 1276 | revlist.append(recipe_dep) | 1279 | revlist.append(recipe_dep) |
| 1277 | revs[recipe.id] = revlist | 1280 | revs[recipe.id] = revlist |
| 1278 | 1281 | ||
| 1279 | build = Build.objects.get(pk=build_id) | ||
| 1280 | |||
| 1281 | context = { | 1282 | context = { |
| 1282 | 'objectname': 'recipes', | 1283 | 'objectname': 'recipes', |
| 1283 | 'build': build, | 1284 | 'build': build, |
