Add buttons to build dashboard to customise/edit images
cover Add buttons to build dashboard to customise/edit images
Add buttons to the build dashboard which enable a user to customise an image built during that build, or to create a custom image from an image built during that build.
To test:
1. Create a project.
2. Build an image like core-image-minimal.
3. When the build completes, go to its build dashboard. Click the button in the left navigation which creates a custom image from the built image.
4. Run a build like "core-image-minimal core-image-sato".
5. In the build dashboard again, click on the button to create a custom image from the images built during that build. You should see a dialog with radio buttons allowing you to choose which image to customise.
6. Create a custom image.
7. Run a build of that custom image.
8. When the build completes, view its build dashboard. It should have a button which links to the custom image edit page for the image.
9. Create a second custom image.
10. Run a build which builds both of your custom images.
11. When the build completes, view its build dashboard. It should have a button which, when clicked, gives a choice of the two custom images for editing.
Repeat the above with any combinations of the 4 images you've already built. Note that the UI tests cover the above combinations.
The following changes since commit 4bb6fb30b0d660eaeaf4af134b99b2feaf0b3db2:
toaster: fixes for customimage package not found (2016-04-08 09:38:50 +0100)
are available in the git repository at:
git://git.yoctoproject.org/poky-contrib elliot/toaster/build_history_ic_links_with_tests-9123
http://git.yoctoproject.org/cgit.cgi/poky-contrib/log/?h=elliot/toaster/build_history_ic_links_with_tests-9123
Related bug: https://bugzilla.yoctoproject.org/show_bug.cgi?id=9123
Elliot Smith (3):
toaster: add build dashboard buttons to edit/create custom images
toaster: add modal to select custom image for editing
toaster-tests: tests for build dashboard
bitbake/lib/toaster/orm/models.py | 48 ++++ .../tests/browser/test_builddashboard_page.py | 251 +++++++++++++++++++++ .../lib/toaster/toastergui/static/js/layerBtn.js | 3 +- .../lib/toaster/toastergui/static/js/libtoaster.js | 2 + .../toastergui/static/js/newcustomimage_modal.js | 98 +++++++- .../toaster/toastergui/static/js/recipedetails.js | 3 +- bitbake/lib/toaster/toastergui/templates/base.html | 1 - .../toastergui/templates/basebuildpage.html | 213 +++++++++++------ .../templates/editcustomimage_modal.html | 71 ++++++ .../toastergui/templates/newcustomimage_modal.html | 28 ++- .../templatetags/objects_to_dictionaries_filter.py | 35 +++ bitbake/lib/toaster/toastergui/tests.py | 87 ------- bitbake/lib/toaster/toastergui/views.py | 33 ++- 13 files changed, 691 insertions(+), 182 deletions(-) create mode 100644 bitbake/lib/toaster/tests/browser/test_builddashboard_page.py create mode 100644 bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html create mode 100644 bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py
--
1.9.3
---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47
This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). Any review or distribution by others is strictly prohibited. If you are not the intended recipient, please contact the sender and delete all copies.
PATCH 1/3 toaster: add build dashboard buttons to edit/create custom images
When a build is viewed in the dashboard, enable users to edit a custom image which was built during that build, and/or create a new custom image based on one of the image recipes built during the build.
Add methods to the Build model to enable querying for the set of image recipes built during a build.
Add buttons to the dashboard, with the "Edit custom image" button opening a basic modal for now. The "New custom image" button opens the existing new custom image modal, but is modified to show a list of images available as a base for a new custom image.
Add a new function to the new custom image modal's script which enables multiple potential custom images to be shown as radio buttons in the dialog (if there is more than 1). Modify existing code to use this new function.
Add a template filter which allows the queryset of recipes for a build to be available to client-side scripts, and from there be used to populate the new custom image modal.
[YOCTO #9123]
--- bitbake/lib/toaster/orm/models.py | 41 ++++ .../lib/toaster/toastergui/static/js/layerBtn.js | 3 +- .../toastergui/static/js/newcustomimage_modal.js | 97 +++++++++- .../toaster/toastergui/static/js/recipedetails.js | 3 +- .../toastergui/templates/basebuildpage.html | 207 +++++++++++++-------- .../templates/editcustomimage_modal.html | 23 +++ .../toastergui/templates/newcustomimage_modal.html | 28 ++- .../templatetags/queryset_to_list_filter.py | 26 +++ bitbake/lib/toaster/toastergui/views.py | 7 +- 9 files changed, 344 insertions(+), 91 deletions(-) create mode 100644 bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html create mode 100644 bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py
bitbake/lib/toaster/orm/models.py +41 -0
@@ -484,6 +484,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]
bitbake/lib/toaster/toastergui/static/js/layerBtn.js +2 -1
@@ -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'); }); }
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +91 -6
142 lines not on this page · raw message
bitbake/lib/toaster/toastergui/static/js/recipedetails.js +2 -1
@@ -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'); });
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +133 -74
223 lines not on this page · raw message
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html new file +23 -0
@@ -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>
bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html +22 -6
@@ -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>
bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py new file +26 -0
@@ -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)
bitbake/lib/toaster/toastergui/views.py +4 -3
@@ -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,
Signature · 12 lines
1.9.3 --------------------------------------------------------------------- Intel Corporation (UK) Limited Registered No. 1134945 (England) Registered Office: Pipers Way, Swindon SN3 1RJ VAT No: 860 2173 47 This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). Any review or distribution by others is strictly prohibited. If you are not the intended recipient, please contact the sender and delete all copies.
Re: [PATCH 1/3] toaster: add build dashboard buttons to edit/create custom images
Sent to bitbake-devel and added to toaster-next
Thanks,
Michael
Quoted 628 lines · Elliot Smith
When a build is viewed in the dashboard, enable users to edit a custom image which was built during that build, and/or create a new custom image based on one of the image recipes built during the build.
Add methods to the Build model to enable querying for the set of image recipes built during a build.
Add buttons to the dashboard, with the "Edit custom image" button opening a basic modal for now. The "New custom image" button opens the existing new custom image modal, but is modified to show a list of images available as a base for a new custom image.
Add a new function to the new custom image modal's script which enables multiple potential custom images to be shown as radio buttons in the dialog (if there is more than 1). Modify existing code to use this new function.
Add a template filter which allows the queryset of recipes for a build to be available to client-side scripts, and from there be used to populate the new custom image modal.
[YOCTO #9123]
Signed-off-by Elliot Smith <[email protected]>--- bitbake/lib/toaster/orm/models.py | 41 ++++ .../lib/toaster/toastergui/static/js/layerBtn.js | 3 +- .../toastergui/static/js/newcustomimage_modal.js | 97 +++++++++- .../toaster/toastergui/static/js/recipedetails.js | 3 +- .../toastergui/templates/basebuildpage.html | 207 +++++++++++++-------- .../templates/editcustomimage_modal.html | 23 +++ .../toastergui/templates/newcustomimage_modal.html | 28 ++- .../templatetags/queryset_to_list_filter.py | 26 +++ bitbake/lib/toaster/toastergui/views.py | 7 +- 9 files changed, 344 insertions(+), 91 deletions(-)create mode 100644 bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html create mode 100644
bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.pybitbake/lib/toaster/orm/models.py +41
-0@@ -484,6 +484,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]bitbake/lib/toaster/toastergui/static/js/layerBtn.js +2
-1@@ -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'); }); }bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +91
-6@@ -1,29 +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 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); + showNameError(invalidNameMsg); + return; } else if (ret.error === "already-exists") { - showError("An image with this name already exists. Image names must be unique."); + showNameError(duplicateNameMsg); + return; } } else { imgCustomModal.modal('hide');@@ -33,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);@@ -46,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 {@@ -56,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(); + } +}bitbake/lib/toaster/toastergui/static/js/recipedetails.js +2
-1@@ -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'); });bitbake/lib/toaster/toastergui/templates/basebuildpage.html +133
-74@@ -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 %}bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html new file +23
-0@@ -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>bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html +22
-6@@ -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>bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py new file +26
-0@@ -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)bitbake/lib/toaster/toastergui/views.py +4
-3@@ -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,
PATCH 2/3 toaster: add modal to select custom image for editing
In reply to Elliot SmithAdd functionality to the placeholder button on the build dashboard to open a modal dialog displaying editable custom images, in cases where multiple custom images were built by the build. Where there is only one editable custom image, go direct to its edit page.
The images shown in the modal are custom recipes for the project which were built during the build shown in the dashboard.
This also affects the new custom image dialog, as that also has to show custom image recipes as well as image recipes built during the build. Modify the API on the Build object to support both.
Also modify and rename the queryset_to_list template filter so that it can deal with lists as well as querysets, as the new custom image modal has to show a list of image recipes which is an amalgam of two querysets.
[YOCTO #9213]
--- bitbake/lib/toaster/orm/models.py | 45 ++++++++------ .../lib/toaster/toastergui/static/js/libtoaster.js | 2 + .../toastergui/static/js/newcustomimage_modal.js | 7 ++- bitbake/lib/toaster/toastergui/templates/base.html | 1 - .../toastergui/templates/basebuildpage.html | 62 +++++++++++--------- .../templates/editcustomimage_modal.html | 68 ++++++++++++++++++---- .../templatetags/objects_to_dictionaries_filter.py | 35 +++++++++++ .../templatetags/queryset_to_list_filter.py | 26 --------- bitbake/lib/toaster/toastergui/views.py | 26 +++++++-- 9 files changed, 182 insertions(+), 90 deletions(-) create mode 100644 bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py delete mode 100644 bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py
bitbake/lib/toaster/orm/models.py +26 -19
@@ -497,33 +497,37 @@ class Build(models.Model): 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 + Returns a list of image Recipes (custom and built-in) related to this + build, sorted by name; note that this has to be done in two steps, as + there's no way to get all the custom image recipes and image recipes + in one query """ - criteria = Q(is_image=True) - return self.get_recipes().filter(criteria).order_by('name') + custom_image_recipes = self.get_custom_image_recipes() + custom_image_recipe_names = custom_image_recipes.values_list('name', flat=True) + + not_custom_image_recipes = ~Q(name__in=custom_image_recipe_names) & \ + Q(is_image=True) + + built_image_recipes = self.get_recipes().filter(not_custom_image_recipes) + + # append to the custom image recipes and sort + customisable_image_recipes = list( + itertools.chain(custom_image_recipes, built_image_recipes) + ) + + return sorted(customisable_image_recipes, key=lambda recipe: recipe.name) def get_custom_image_recipes(self): """ - Returns a queryset of custom image recipes related to this build, + Returns a queryset of CustomImageRecipes 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') + built_recipe_names = self.get_recipes().values_list('name', flat=True) + criteria = Q(name__in=built_recipe_names) & Q(project=self.project) + queryset = CustomImageRecipe.objects.filter(criteria).order_by('name') + return queryset def get_outcome_text(self): return Build.BUILD_OUTCOME[int(self.outcome)][1]
@@ -1374,6 +1378,9 @@ class Layer(models.Model): # LayerCommit class is synced with layerindex.LayerBranch class Layer_Version(models.Model): + """ + A Layer_Version either belongs to a single project or no project + """ search_allowed_fields = ["layer__name", "layer__summary", "layer__description", "layer__vcs_url", "dirpath", "up_branch__name", "commit", "branch"] build = models.ForeignKey(Build, related_name='layer_version_build', default = None, null = True) layer = models.ForeignKey(Layer, related_name='layer_version_layer')
bitbake/lib/toaster/toastergui/static/js/libtoaster.js +2 -0
@@ -344,6 +344,8 @@ var libtoaster = (function (){ } function _createCustomRecipe(name, baseRecipeId, doneCb){ + debugger; + var data = { 'name' : name, 'project' : libtoaster.ctx.projectId,
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +4 -3
@@ -12,6 +12,7 @@ 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");
@@ -112,13 +113,13 @@ function newCustomImageModalSetRecipes(baseRecipes) { var imageSelector = $('#new-custom-image-modal [data-role="image-selector"]'); var imageSelectRadiosContainer = $('#new-custom-image-modal [data-role="image-selector-radios"]'); + // remove any existing radio buttons + labels + imageSelector.remove('[data-role="image-radio"]'); + 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);
bitbake/lib/toaster/toastergui/templates/base.html +0 -1
@@ -43,7 +43,6 @@ 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}}, - projectBuildsUrl: {% url 'projectbuilds' project.id as pburl %}{{pburl|json}}, xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}", projectId : {{project.id}},
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +35 -27
88 lines not on this page · raw message
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +58 -10
81 lines not on this page · raw message
bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py new file +35 -0
@@ -0,0 +1,35 @@ +from django import template +import json + +register = template.Library() + +def objects_to_dictionaries(iterable, fields): + """ + Convert an iterable into a list of dictionaries; fields should be set + to a comma-separated string of properties for each item included in the + resulting list; e.g. for a queryset: + + {{ queryset | objects_to_dictionaries:"id,name" }} + + will return a list like + + [{'id': 1, 'name': 'foo'}, ...] + + providing queryset has id and name fields + + This is mostly to support serialising querysets or lists of model objects + to JSON + """ + objects = [] + + if fields: + fields_list = [field.strip() for field in fields.split(',')] + for item in iterable: + out = {} + for field in fields_list: + out[field] = getattr(item, field) + objects.append(out) + + return objects + +register.filter('objects_to_dictionaries', objects_to_dictionaries)
bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py deleted +0 -26
@@ -1,26 +0,0 @@ -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)
bitbake/lib/toaster/toastergui/views.py +22 -4
@@ -507,6 +507,7 @@ def builddashboard( request, build_id ): context = { 'build' : build, + 'project' : build.project, 'hasImages' : hasImages, 'ntargets' : ntargets, 'targets' : targets,
@@ -797,6 +798,7 @@ eans multiple licenses exist that cover different parts of the source', context = { 'objectname': variant, 'build' : build, + 'project' : build.project, 'target' : Target.objects.filter( pk = target_id )[ 0 ], 'objects' : packages, 'packages_sum' : packages_sum[ 'installed_size__sum' ],
@@ -937,7 +939,10 @@ def dirinfo(request, build_id, target_id, file_path=None): if head != sep: dir_list.insert(0, head) - context = { 'build': Build.objects.get(pk=build_id), + build = Build.objects.get(pk=build_id) + + context = { 'build': build, + 'project': build.project, 'target': Target.objects.get(pk=target_id), 'packages_sum': packages_sum['installed_size__sum'], 'objects': objects,
@@ -1211,6 +1216,7 @@ def tasks_common(request, build_id, variant, task_anchor): 'filter_search_display': filter_search_display, 'mainheading': title_variant, 'build': build, + 'project': build.project, 'objects': task_objects, 'default_orderby' : orderby, 'search_term': search_term,
@@ -1282,6 +1288,7 @@ def recipes(request, build_id): context = { 'objectname': 'recipes', 'build': build, + 'project': build.project, 'objects': recipes, 'default_orderby' : 'name:+', 'recipe_deps' : deps,
@@ -1366,10 +1373,12 @@ def configuration(request, build_id): 'MACHINE', 'DISTRO', 'DISTRO_VERSION', 'TUNE_FEATURES', 'TARGET_FPU') context = dict(Variable.objects.filter(build=build_id, variable_name__in=var_names)\ .values_list('variable_name', 'variable_value')) + build = Build.objects.get(pk=build_id) context.update({'objectname': 'configuration', 'object_search_display':'variables', 'filter_search_display':'variables', - 'build': Build.objects.get(pk=build_id), + 'build': build, + 'project': build.project, 'targets': Target.objects.filter(build=build_id)}) return render(request, template, context)
@@ -1406,12 +1415,15 @@ def configvars(request, build_id): file_filter += '/bitbake.conf' build_dir=re.sub("/tmp/log/.*","",Build.objects.get(pk=build_id).cooker_log_path) + build = Build.objects.get(pk=build_id) + context = { 'objectname': 'configvars', 'object_search_display':'BitBake variables', 'filter_search_display':'variables', 'file_filter': file_filter, - 'build': Build.objects.get(pk=build_id), + 'build': build, + 'project': build.project, 'objects' : variables, 'total_count':queryset_with_search.count(), 'default_orderby' : 'variable_name:+',
@@ -1480,6 +1492,7 @@ def bpackage(request, build_id): context = { 'objectname': 'packages built', 'build': build, + 'project': build.project, 'objects' : packages, 'default_orderby' : 'name:+', 'tablecols':[
@@ -1554,7 +1567,12 @@ def bpackage(request, build_id): def bfile(request, build_id, package_id): template = 'bfile.html' files = Package_File.objects.filter(package = package_id) - context = {'build': Build.objects.get(pk=build_id), 'objects' : files} + build = Build.objects.get(pk=build_id) + context = { + 'build': build, + 'project': build.project, + 'objects' : files + } return render(request, template, context)
Signature · 12 lines
1.9.3 --------------------------------------------------------------------- Intel Corporation (UK) Limited Registered No. 1134945 (England) Registered Office: Pipers Way, Swindon SN3 1RJ VAT No: 860 2173 47 This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). Any review or distribution by others is strictly prohibited. If you are not the intended recipient, please contact the sender and delete all copies.
Re: [PATCH 2/3] toaster: add modal to select custom image for editing
Hi Elliot,
While reviewing open RRs, I noticed that you transposed the yocto number. It should be #9123. Dave
Quoted 19 lines
-----Original Message-----
From: [email protected] [mailto:[email protected]] On Behalf Of Elliot Smith
Sent: Monday, April 11, 2016 9:56 AM
To: [email protected]
Subject: [Toaster] [PATCH 2/3] toaster: add modal to select custom image for editingAdd functionality to the placeholder button on the build dashboard to open a modal dialog displaying editable custom images, in cases where multiple custom images were built by the build. Where there is only one editable custom image, go direct to its edit page.
The images shown in the modal are custom recipes for the project which were built during the build shown in the dashboard.
This also affects the new custom image dialog, as that also has to show custom image recipes as well as image recipes built during the build. Modify the API on the Build object to support both.
Quoted 6 lines
Also modify and rename the
queryset_to_listtemplate filter so that it can deal with lists as well as querysets, as the new custom image modal has to show a list of image recipes which is an amalgam of two querysets.[YOCTO #9213]
Bug 9213 - Enable thumb for ARM builds
Bug 9123 - Build history pages are missing the image customisation links
Quoted 544 lines
Signed-off-by Elliot Smith <[email protected]>--- bitbake/lib/toaster/orm/models.py | 45 ++++++++------ .../lib/toaster/toastergui/static/js/libtoaster.js | 2 + .../toastergui/static/js/newcustomimage_modal.js | 7 ++- bitbake/lib/toaster/toastergui/templates/base.html | 1 - .../toastergui/templates/basebuildpage.html | 62 +++++++++++--------- .../templates/editcustomimage_modal.html | 68 ++++++++++++++++++---- .../templatetags/objects_to_dictionaries_filter.py | 35 +++++++++++ .../templatetags/queryset_to_list_filter.py | 26 --------- bitbake/lib/toaster/toastergui/views.py | 26 +++++++-- 9 files changed, 182 insertions(+), 90 deletions(-) create mode 100644 bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py delete mode 100644
bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.pybitbake/lib/toaster/orm/models.py +26
-19@@ -497,33 +497,37 @@ class Build(models.Model): 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 + Returns a list of image Recipes (custom and built-in) related to this + build, sorted by name; note that this has to be done in two steps, as + there's no way to get all the custom image recipes and image recipes + in one query """ - criteria = Q(is_image=True) - return self.get_recipes().filter(criteria).order_by('name') + custom_image_recipes = self.get_custom_image_recipes() + custom_image_recipe_names = custom_image_recipes.values_list('name', flat=True) + + not_custom_image_recipes = ~Q(name__in=custom_image_recipe_names) & \ + Q(is_image=True) + + built_image_recipes = self.get_recipes().filter(not_custom_image_recipes) + + # append to the custom image recipes and sort + customisable_image_recipes = list( + itertools.chain(custom_image_recipes, built_image_recipes) + ) + + return sorted(customisable_image_recipes, key=lambda recipe: recipe.name) def get_custom_image_recipes(self): """ - Returns a queryset of custom image recipes related to this build, + Returns a queryset of CustomImageRecipes 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') + built_recipe_names = self.get_recipes().values_list('name', flat=True) + criteria = Q(name__in=built_recipe_names) & Q(project=self.project) + queryset = CustomImageRecipe.objects.filter(criteria).order_by('name') + return queryset def get_outcome_text(self): return Build.BUILD_OUTCOME[int(self.outcome)][1]@@ -1374,6 +1378,9 @@ class Layer(models.Model): # LayerCommit class is synced with layerindex.LayerBranch class Layer_Version(models.Model): + """ + A Layer_Version either belongs to a single project or no project + """ search_allowed_fields = ["layer__name", "layer__summary", "layer__description","
layer__vcs_url", "dirpath", "up_branch__name", "commit", "branch"]
build = models.ForeignKey(Build, related_name='layer_version_build', default =
None, null = True)
layer =models.ForeignKey(Layer, related_name='layer_version_layer')bitbake/lib/toaster/toastergui/static/js/libtoaster.js +2
-0@@ -344,6 +344,8 @@ var libtoaster = (function (){ } function _createCustomRecipe(name, baseRecipeId, doneCb){ + debugger; + var data = { 'name' : name, 'project' : libtoaster.ctx.projectId,bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +0
-0@@ -12,6 +12,7 @@ for the new custom image. This will manage the addition of radiobuttons
to select the base image (or remove the radio buttons, if there is only a single base image available).
*/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +4
-3+ function newCustomImageModalInit(){ var newCustomImgBtn = $("#create-new-custom-image-btn");@@ -112,13 +113,13 @@ function newCustomImageModalSetRecipes(baseRecipes) { var imageSelector = $('#new-custom-image-modal [data-role="image-selector"]'); var imageSelectRadiosContainer = $('#new-custom-image-modal [data-role="image- selector-radios"]'); + // remove any existing radio buttons + labels + imageSelector.remove('[data-role="image-radio"]'); + 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 fromimgCustomModal.data('recipe', baseRecipes[0].id);bitbake/lib/toaster/toastergui/templates/base.html +0
-1@@ -43,7 +43,6 @@ 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}}, -projectBuildsUrl: {% url 'projectbuilds' project.id as pburl %}{{pburl|json}}, xhrCustomRecipeUrl : "{% url '
xhr_customrecipe' %}",
projectId : {{project.id}},bitbake/lib/toaster/toastergui/templates/basebuildpage.html +19
-26@@ -1,7 +1,7 @@ {% extends "base.html" %} {% load projecttags %} {% load project_url_tag %} -{% load queryset_to_list_filter %} +{% load objects_to_dictionaries_filter %} {% load humanize %} {% block pagecontent %} <!-- breadcrumbs -->@@ -81,33 +81,40 @@ </p> </li> - <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 }}; - - // edit custom image which was built during this build - var editCustomImageModal = $('#edit-custom-image-modal'); - var editCustomImageTrigger = $('[data-role="edit-custom-image- trigger"]'); + {% with build.get_custom_image_recipes as custom_image_recipes %} + {% if custom_image_recipes.count > 0 %} + <!-- edit custom image built during this build --> + <li> + <p class="navbar-btn" data-role="edit-custom-image-trigger"> + <button class="btn btn-block">Edit custom image</button> + {% include 'editcustomimage_modal.html' %} + <script> + var editableCustomImageRecipes = {{ custom_image_recipes | objects_to_dictionaries:"id,name" | json }}; - 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 + $(document).ready(function () { + var editCustomImageTrigger = $('[data-role="edit-custom-image- trigger"]'); + var editCustomImageModal = $('#edit-custom-image-modal'); - // single editable custom image - - // multiple editable custom images - editCustomImageModal.modal('show'); - }); - }); - </script> - </li> + // edit custom image which was built during this build + editCustomImageTrigger.click(function () { + // single editable custom image: redirect to the edit page + // for that image + if (editableCustomImageRecipes.length === 1) { + var url = '{% url "customrecipe" build.project.idcustom_image_recipes.first.id %}';
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +15
-1+ document.location.href = url; + } + // multiple editable custom images: show modal to select + // one of them for editing + else { + editCustomImageModal.modal('show'); + } + }); + }); + </script> + </p> + </li> + {% endif %} + {% endwith %} <li> <!-- new custom image from image recipe in this build -->@@ -119,7 +126,7 @@ // 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 }}; + var imageRecipes = {{ build.get_image_recipes | objects_to_dictionaries:"id,name" | json }};$(document).ready(function () { var newCustomImageModal = $('#new-custom-image-modal');bitbake/lib/toaster/toastergui/templates/basebuildpage.html +1
-0@@ -131,6 +138,7 @@ if (!imageRecipes.length) { return; } + newCustomImageModalSetRecipes(imageRecipes); newCustomImageModal.modal('show'); });bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +6
-1@@ -1,23 +1,71 @@ <!-- -modal dialog shown on the build dashboard, for editing an existing custom image +modal dialog shown on the build dashboard, for editing an existing custom image; +only shown if more than one custom image was built, so the user needs to +choose which one to edit + +required context: + build - a Build object --> <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>
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +52
-9- <h3>Select custom image to edit</h3> + <h3>Which image do you want 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> + {% for recipe in build.get_custom_image_recipes %} + <label class="radio"> + {{recipe.name}} + <input type="radio" class="form-control" name="select-custom-image" + data-url="{% url 'customrecipe' build.project.id recipe.id %}"> + </label> + {% endfor %} </div> + <span class="help-block error" id="invalid-custom-image-help" style="display:none"> + Please select a custom image to edit. + </span> </div> + <div class="modal-footer"> - <button class="btn btn-primary btn-large" disabled>Action</button> + <button class="btn btn-primary btn-large" data-url="#" + data-action="edit-custom-image" disabled> + Edit custom image + </button> </div> </div> + +<script> +$(document).ready(function () { + var editCustomImageButton = $('[data-action="edit-custom-image"]'); + var error = $('#invalid-custom-image-help'); + var radios = $('[name="select-custom-image"]'); + + // return custom image radio buttons which are selected + var getSelectedRadios = function () { + return $('[name="select-custom-image"]:checked'); + }; + + radios.change(function () { + if (getSelectedRadios().length === 1) { + editCustomImageButton.removeAttr('disabled'); + error.hide(); + } + else { + editCustomImageButton.attr('disabled', 'disabled'); + error.show(); + } + }); + + editCustomImageButton.click(function () { + var selectedRadios = getSelectedRadios(); + + if (selectedRadios.length === 1) { + document.location.href = selectedRadios.first().attr('data-url'); + } + else { + error.show(); + } + }); +}); +</script>diff --git
a/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.pyb/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.pynew file mode 100644
index 0000000..0dcc7d2bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py new file +35
-0@@ -0,0 +1,35 @@ +from django import template +import json + +register = template.Library() + +def objects_to_dictionaries(iterable, fields): + """ + Convert an iterable into a list of dictionaries; fields should be set + to a comma-separated string of properties for each item included in the + resulting list; e.g. for a queryset: + + {{ queryset | objects_to_dictionaries:"id,name" }} + + will return a list like + + [{'id': 1, 'name': 'foo'}, ...] + + providing queryset has id and name fields + + This is mostly to support serialising querysets or lists of model objects + to JSON + """ + objects = [] + + if fields: + fields_list = [field.strip() for field in fields.split(',')] + for item in iterable: + out = {} + for field in fields_list: + out[field] = getattr(item, field) + objects.append(out) + + return objects + +register.filter('objects_to_dictionaries', objects_to_dictionaries)bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py deleted +0
-26@@ -1,26 +0,0 @@ -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)bitbake/lib/toaster/toastergui/views.py +2
-0@@ -507,6 +507,7 @@ def builddashboard( request, build_id ): context = { 'build' : build, + 'project' : build.project, 'hasImages' : hasImages, 'ntargets' : ntargets, 'targets' : targets,@@ -797,6 +798,7 @@ eans multiple licenses exist that cover different parts of the source', context = { 'objectname': variant, 'build' : build, + 'project' : build.project, 'target' : Target.objects.filter( pk = target_id )[ 0 ], 'objects' : packages,'
packages_sum' : packages_sum[ 'installed_size__sum' ],bitbake/lib/toaster/toastergui/views.py +9
-2@@ -937,7 +939,10 @@ def dirinfo(request, build_id, target_id, file_path=None): if head != sep: dir_list.insert(0, head) - context = { 'build': Build.objects.get(pk=build_id), + build = Build.objects.get(pk=build_id) + + context = { 'build': build, + 'project': build.project, 'target': Target.objects.get(pk=target_id), 'packages_sum': packages_sum['installed_size__sum'], 'objects': objects,@@ -1211,6 +1216,7 @@ def tasks_common(request, build_id, variant, task_anchor): 'filter_search_display': filter_search_display, 'mainheading': title_variant, 'build': build, + 'project': build.project, 'objects': task_objects, 'default_orderby' : orderby, 'search_term': search_term,@@ -1282,6 +1288,7 @@ def recipes(request, build_id): context = { 'objectname': 'recipes', 'build': build, + 'project': build.project, 'objects': recipes, 'default_orderby' : 'name:+', 'recipe_deps' : deps,@@ -1366,10 +1373,12 @@ def configuration(request, build_id): 'MACHINE', 'DISTRO', 'DISTRO_VERSION', 'TUNE_FEATURES', 'TARGET_FPU') context = dict(Variable.objects.filter(build=build_id, variable_name__in=var_names)\ .values_list('variable_name', 'variable_value')) + build = Build.objects.get(pk=build_id) context.update({'objectname': 'configuration', 'object_search_display':'variables', 'filter_search_display':'variables', - 'build': Build.objects.get(pk=build_id), + 'build': build, + 'project': build.project, 'targets': Target.objects.filter(build=build_id)})return
render(request, template, context)bitbake/lib/toaster/toastergui/views.py +11
-3@@ -1406,12 +1415,15 @@ def configvars(request, build_id): file_filter += '/bitbake.conf' build_dir=re.sub("/tmp/log/.*","",Build.objects.get(pk=build_id).cooker_log_path) + build = Build.objects.get(pk=build_id) + context = { 'objectname': 'configvars', 'object_search_display':'BitBake variables', 'filter_search_display':'variables', 'file_filter': file_filter, - 'build': Build.objects.get(pk=build_id), + 'build': build, + 'project': build.project, 'objects' : variables, 'total_count':queryset_with_search.count(), 'default_orderby' : 'variable_name:+',@@ -1480,6 +1492,7 @@ def bpackage(request, build_id): context = { 'objectname': 'packages built', 'build': build, + 'project': build.project, 'objects' : packages, 'default_orderby' : 'name:+', 'tablecols':[@@ -1554,7 +1567,12 @@ def bpackage(request, build_id): def bfile(request, build_id, package_id): template = 'bfile.html' files = Package_File.objects.filter(package = package_id) - context = {'build': Build.objects.get(pk=build_id), 'objects' : files} + build = Build.objects.get(pk=build_id) + context = { + 'build': build, + 'project': build.project, + 'objects' : files + } return render(request, template, context) --1.9.3
---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). Any review or distribution by others is strictly prohibited. If you are not the intended recipient, please contact the sender and delete all copies. --
_______________________________________________
toaster mailing list
[email protected]
https://lists.yoctoproject.org/listinfo/toaster
Re: [PATCH 2/3] toaster: add modal to select custom image for editing
Thanks Dave, nice spot. I will fix it in the next version I submit for review.
Elliot
Quoted 651 lines · David M (Wind River)
Hi Elliot,
While reviewing open RRs, I noticed that you transposed the yocto number. It should be #9123.
DaveQuoted 2 lines
-----Original Message-----
From: [email protected] [mailto:Quoted 4 lines
Behalf Of Elliot Smith
Sent: Monday, April 11, 2016 9:56 AM
To: [email protected]
Subject: [Toaster] [PATCH 2/3] toaster: add modal to select custom imagefor editing
Quoted 13 lines
Add functionality to the placeholder button on the build dashboard to open a modal dialog displaying editable custom images, in cases where multiple custom images were built by the build. Where there is only one editable custom image, go direct to its edit page.
The images shown in the modal are custom recipes for the project which were built during the build shown in the dashboard.
This also affects the new custom image dialog, as that also has to show custom image recipes as well as image recipes built during the build. Modify the API on the Build object to support both.
Quoted 6 lines
Also modify and rename the
queryset_to_listtemplate filter so that it can deal with lists as well as querysets, as the new custom image modal has to show a list of image recipes which is an amalgam of two querysets.[YOCTO #9213]
Bug 9213 - Enable thumb for ARM builds
Bug 9123 - Build history pages are missing the image customisation linksQuoted 8 lines
Signed-off-by Elliot Smith <[email protected]>--- bitbake/lib/toaster/orm/models.py | 45 ++++++++------ .../lib/toaster/toastergui/static/js/libtoaster.js | 2 + .../toastergui/static/js/newcustomimage_modal.js | 7 ++- bitbake/lib/toaster/toastergui/templates/base.html | 1 - .../toastergui/templates/basebuildpage.html | 62+++++++++++---------
Quoted 1 line
.../templates/editcustomimage_modal.html | 68++++++++++++++++++----
Quoted 6 lines
.../templatetags/objects_to_dictionaries_filter.py | 35 +++++++++++ .../templatetags/queryset_to_list_filter.py | 26 --------- bitbake/lib/toaster/toastergui/views.py | 26 +++++++-- 9 files changed, 182 insertions(+), 90 deletions(-) create mode 100644
bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.pyReview
Quoted 4 lines
delete mode 100644
bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py(quoted diff) +0
-0
b/bitbake/lib/toaster/orm/models.pybitbake/lib/toaster/orm/models.py class Build(models.Model): · line 498
Quoted 6 lines
index c63d631..a146541 100644
bitbake/lib/toaster/orm/models.py +0
-0@@ -497,33 +497,37 @@ class Build(models.Model): return Recipe.objects.filter(criteria) \ .select_related('layer_version','
layer_version__layer')bitbake/lib/toaster/orm/models.py class Build(models.Model):
Quoted 5 lines
bitbake/lib/toaster/orm/models.py +0
-4- 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 fromthe
bitbake/lib/toaster/orm/models.py class Build(models.Model): · line 503
Quoted 1 line
bitbake/lib/toaster/orm/models.py +0
-1- recipes for the build by name, and to distinguish image recipesfrom
bitbake/lib/toaster/orm/models.py class Build(models.Model):
Quoted 9 lines
bitbake/lib/toaster/orm/models.py +0
-7- 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
bitbake/lib/toaster/orm/models.py class Build(models.Model): · line 501
Quoted 2 lines
bitbake/lib/toaster/orm/models.py +1
-1- by name + Returns a list of image Recipes (custom and built-in) relatedto this
bitbake/lib/toaster/orm/models.py class Build(models.Model): · line 502
Quoted 1 line
bitbake/lib/toaster/orm/models.py +1
-0+ build, sorted by name; note that this has to be done in twosteps, as
bitbake/lib/toaster/orm/models.py class Build(models.Model): · line 503
Quoted 1 line
bitbake/lib/toaster/orm/models.py +1
-0+ there's no way to get all the custom image recipes and imagerecipes
bitbake/lib/toaster/orm/models.py class Build(models.Model):
Quoted 6 lines
bitbake/lib/toaster/orm/models.py +3
-2+ in one query """ - criteria = Q(is_image=True) - return self.get_recipes().filter(criteria).order_by('name') + custom_image_recipes = self.get_custom_image_recipes() + custom_image_recipe_names =
custom_image_recipes.values_list('name', flat=True)bitbake/lib/toaster/orm/models.py class Build(models.Model): · line 509
Quoted 2 lines
bitbake/lib/toaster/orm/models.py +2
-0+ + not_custom_image_recipes =~Q(name__in=custom_image_recipe_names) & \
bitbake/lib/toaster/orm/models.py class Build(models.Model):
Quoted 3 lines
bitbake/lib/toaster/orm/models.py +3
-0+ Q(is_image=True) + + built_image_recipes =self.get_recipes().filter(not_custom_image_recipes)
bitbake/lib/toaster/orm/models.py class Build(models.Model):
Quoted 7 lines
bitbake/lib/toaster/orm/models.py +7
-0+ + # append to the custom image recipes and sort + customisable_image_recipes = list( + itertools.chain(custom_image_recipes, built_image_recipes) + ) + + return sorted(customisable_image_recipes, key=lambda recipe:recipe.name)
bitbake/lib/toaster/orm/models.py class Build(models.Model):
Quoted 4 lines
bitbake/lib/toaster/orm/models.py +0
-1def get_custom_image_recipes(self): """ - Returns a queryset of custom image recipes related to thisbuild,
bitbake/lib/toaster/orm/models.py class Build(models.Model):
Quoted 5 lines
bitbake/lib/toaster/orm/models.py +1
-2+ Returns a queryset of CustomImageRecipes 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)bitbake/lib/toaster/orm/models.py class Build(models.Model): · line 525
Quoted 2 lines
bitbake/lib/toaster/orm/models.py +1
-1- return self.get_recipes().filter(criteria).order_by('name') + built_recipe_names = self.get_recipes().values_list('name',flat=True)
bitbake/lib/toaster/orm/models.py class Build(models.Model): · line 526
Quoted 1 line
bitbake/lib/toaster/orm/models.py +1
-0+ criteria = Q(name__in=built_recipe_names) &Q(project=self.project)
bitbake/lib/toaster/orm/models.py class Build(models.Model): · line 527
Quoted 1 line
bitbake/lib/toaster/orm/models.py +1
-0+ queryset =CustomImageRecipe.objects.filter(criteria).order_by('name')
bitbake/lib/toaster/orm/models.py class Layer(models.Model): · line 1384
Quoted 12 lines
bitbake/lib/toaster/orm/models.py +4
-0+ return queryset def get_outcome_text(self): return Build.BUILD_OUTCOME[int(self.outcome)][1]@@ -1374,6 +1378,9 @@ class Layer(models.Model): # LayerCommit class is synced with layerindex.LayerBranch class Layer_Version(models.Model): + """ + A Layer_Version either belongs to a single project or no project + """ search_allowed_fields = ["layer__name", "layer__summary","
layer__description",Quoted 2 lines
"
layer__vcs_url", "dirpath", "up_branch__name", "commit", "branch"]
build = models.ForeignKey(Build,related_name='layer_version_build', default =
Quoted 16 lines
None, null = True)
layer =models.ForeignKey(Layer, related_name='layer_version_layer')bitbake/lib/toaster/toastergui/static/js/libtoaster.js +2
-0@@ -344,6 +344,8 @@ var libtoaster = (function (){ } function _createCustomRecipe(name, baseRecipeId, doneCb){ + debugger; + var data = { 'name' : name, 'project' : libtoaster.ctx.projectId,Quoted 1 line
diff --git
a/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js for the new custom image. This will manage the
Quoted 5 lines
b/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js index 1ae0d34..a6d5b1a 100644
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +0
-0@@ -12,6 +12,7 @@ for the new custom image. This will manage theaddition of radio
Quoted 2 lines
buttons
to select the base image (or remove the radio buttons, if there is onlya
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js function
Quoted 7 lines
single base image available).
*/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +1
-0+ function newCustomImageModalInit(){ var newCustomImgBtn = $("#create-new-custom-image-btn");@@ -112,13 +113,13 @@ functionnewCustomImageModalSetRecipes(baseRecipes) {
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js function · line 113
Quoted 1 line
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +0
-0var imageSelector = $('#new-custom-image-modal[data-role="image-selector"]');
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js function · line 114
Quoted 1 line
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +0
-0var imageSelectRadiosContainer = $('#new-custom-image-modal[data-role="image-
Quoted 21 lines
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +3
-3selector-radios"]'); + // remove any existing radio buttons + labels + imageSelector.remove('[data-role="image-radio"]'); + 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 fromimgCustomModal.data('recipe', baseRecipes[0].id);bitbake/lib/toaster/toastergui/templates/base.html +0
-0bitbake/lib/toaster/toastergui/templates/base.html line 43
Quoted 1 line
bitbake/lib/toaster/toastergui/templates/base.html +0
-0@@ -43,7 +43,6 @@ recipesTypeAheadUrl: {% url 'xhr_recipestypeahead' project.idas
bitbake/lib/toaster/toastergui/templates/base.html line 47
Quoted 4 lines
bitbake/lib/toaster/toastergui/templates/base.html +0
-0paturl%}{{paturl|json}}, layersTypeAheadUrl: {% url 'xhr_layerstypeahead' project.id as paturl%}{{paturl|json}}, machinesTypeAheadUrl: {% url 'xhr_machinestypeahead' project.idas
Quoted 3 lines
bitbake/lib/toaster/toastergui/templates/base.html +0
-1paturl%}{{paturl|json}}, -projectBuildsUrl: {% url 'projectbuilds' project.id as pburl
%}{{pburl|json}},
Quoted 19 lines
xhrCustomRecipeUrl : "{% url '
xhr_customrecipe' %}", projectId : {{project.id}},bitbake/lib/toaster/toastergui/templates/basebuildpage.html +1
-1@@ -1,7 +1,7 @@ {% extends "base.html" %} {% load projecttags %} {% load project_url_tag %} -{% load queryset_to_list_filter %} +{% load objects_to_dictionaries_filter %} {% load humanize %} {% block pagecontent %} <!-- breadcrumbs -->@@ -81,33 +81,40 @@ </p> </li>bitbake/lib/toaster/toastergui/templates/basebuildpage.html line 92
Quoted 10 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +0
-9- <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 |
bitbake/lib/toaster/toastergui/templates/basebuildpage.html
Quoted 5 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +0
-4queryset_to_list:"id,name" | json }}; - - // edit custom image which was built during this build - var editCustomImageModal = $('#edit-custom-image-modal'); - var editCustomImageTrigger =$('[data-role="edit-custom-image-
bitbake/lib/toaster/toastergui/templates/basebuildpage.html line 86
Quoted 2 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +1
-0trigger"]'); + {% with build.get_custom_image_recipes as custom_image_recipes%}
bitbake/lib/toaster/toastergui/templates/basebuildpage.html
Quoted 4 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +4
-0+ {% if custom_image_recipes.count > 0 %} + <!-- edit custom image built during this build --> + <li> + <p class="navbar-btn"data-role="edit-custom-image-trigger">
bitbake/lib/toaster/toastergui/templates/basebuildpage.html
Quoted 4 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +4
-0+ <button class="btn btn-block">Edit custom image</button> + {% include 'editcustomimage_modal.html' %} + <script> + var editableCustomImageRecipes = {{
custom_image_recipes|bitbake/lib/toaster/toastergui/templates/basebuildpage.html
Quoted 4 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +0
-2objects_to_dictionaries:"id,name" | json }}; - editCustomImageTrigger.click(function () { - // if there is a single editable custom image, godirect to the edit
bitbake/lib/toaster/toastergui/templates/basebuildpage.html line 103
Quoted 1 line
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +0
-1- // page for it; if there are multiple editable customimages, show
bitbake/lib/toaster/toastergui/templates/basebuildpage.html
Quoted 3 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +2
-1- // dialog to select one of them for editing + $(document).ready(function () { + var editCustomImageTrigger =$('[data-role="edit-custom-image-
bitbake/lib/toaster/toastergui/templates/basebuildpage.html line 100
Quoted 2 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +1
-0trigger"]'); + var editCustomImageModal =$('#edit-custom-image-modal');
bitbake/lib/toaster/toastergui/templates/basebuildpage.html
Quoted 10 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +1
-8- // single editable custom image - - // multiple editable custom images - editCustomImageModal.modal('show'); - }); - }); - </script> - </li> + // edit custom image which was built during thisbuild
bitbake/lib/toaster/toastergui/templates/basebuildpage.html line 103
Quoted 2 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +2
-0+ editCustomImageTrigger.click(function () { + // single editable custom image: redirect to theedit page
bitbake/lib/toaster/toastergui/templates/basebuildpage.html
Quoted 3 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +3
-0+ // for that image + if (editableCustomImageRecipes.length === 1) { + var url = '{% url "customrecipe"build.project.id
bitbake/lib/toaster/toastergui/templates/basebuildpage.html
Quoted 4 lines
custom_image_recipes.first.id %}';
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +3
-0+ document.location.href = url; + } + // multiple editable custom images: show modal toselect
Quoted 15 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +11
-0+ // one of them for editing + else { + editCustomImageModal.modal('show'); + } + }); + }); + </script> + </p> + </li> + {% endif %} + {% endwith %} <li> <!-- new custom image from image recipe in this build -->bitbake/lib/toaster/toastergui/templates/basebuildpage.html line 126
Quoted 1 line
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +0
-0@@ -119,7 +126,7 @@ // imageRecipes includes both custom image recipes andbuilt-in
bitbake/lib/toaster/toastergui/templates/basebuildpage.html line 127
Quoted 1 line
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +0
-0// image recipes, any of which can be used as the basis fora
bitbake/lib/toaster/toastergui/templates/basebuildpage.html line 122
Quoted 2 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +0
-1// new custom image - var imageRecipes = {{ build.get_image_recipes |queryset_to_list:"id,name"
Quoted 14 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +1
-0| json }}; + var imageRecipes = {{ build.get_image_recipes | objects_to_dictionaries:"id,name" | json }};$(document).ready(function () { var newCustomImageModal = $('#new-custom-image-modal');bitbake/lib/toaster/toastergui/templates/basebuildpage.html +1
-0@@ -131,6 +138,7 @@ if (!imageRecipes.length) { return; } + newCustomImageModalSetRecipes(imageRecipes); newCustomImageModal.modal('show'); });Quoted 1 line
diff --git
a/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html line 2
Quoted 7 lines
b/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html index fd998f6..8046c08 100644
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +0
-1@@ -1,23 +1,71 @@ <!-- -modal dialog shown on the build dashboard, for editing an existingcustom image
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html line 2
Quoted 1 line
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +1
-0+modal dialog shown on the build dashboard, for editing an existingcustom image;
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
Quoted 7 lines
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +5
-0+only shown if more than one custom image was built, so the user needs to +choose which one to edit + +required context: + build - a Build object --> <div class="modal hide fade in" aria-hidden="false"id="edit-custom-image-modal">
Quoted 6 lines
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +0
-0<div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +1
-1- <h3>Select custom image to edit</h3> + <h3>Which image do you want to edit?</h3> </div>bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
Quoted 10 lines
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +1
-7+ <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">Errortext</span>
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
Quoted 4 lines
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +4
-0+ {% for recipe in build.get_custom_image_recipes %} + <label class="radio"> + {{recipe.name}} + <input type="radio" class="form-control"name="select-custom-image"
Quoted 1 line
+ data-url="{% url 'customrecipe' build.project.id
recipe.id %}">
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
Quoted 4 lines
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +3
-0+ </label> + {% endfor %} </div> + <span class="help-block error" id="invalid-custom-image-help"style="display:none">
Quoted 47 lines
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +42
-1+ Please select a custom image to edit. + </span> </div> + <div class="modal-footer"> - <button class="btn btn-primary btn-large" disabled>Action</button> + <button class="btn btn-primary btn-large" data-url="#" + data-action="edit-custom-image" disabled> + Edit custom image + </button> </div> </div> + +<script> +$(document).ready(function () { + var editCustomImageButton = $('[data-action="edit-custom-image"]'); + var error = $('#invalid-custom-image-help'); + var radios = $('[name="select-custom-image"]'); + + // return custom image radio buttons which are selected + var getSelectedRadios = function () { + return $('[name="select-custom-image"]:checked'); + }; + + radios.change(function () { + if (getSelectedRadios().length === 1) { + editCustomImageButton.removeAttr('disabled'); + error.hide(); + } + else { + editCustomImageButton.attr('disabled', 'disabled'); + error.show(); + } + }); + + editCustomImageButton.click(function () { + var selectedRadios = getSelectedRadios(); + + if (selectedRadios.length === 1) { + document.location.href = selectedRadios.first().attr('data-url'); + } + else { + error.show(); + } + }); +}); +</script>Quoted 2 lines
diff --git
a/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.pyQuoted 1 line
b/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.pyQuoted 4 lines
new file mode 100644
index 0000000..0dcc7d2
---/dev/null
+++
b/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.pybitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html line 8
Quoted 9 lines
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +8
-0@@ -0,0 +1,35 @@ +from django import template +import json + +register = template.Library() + +def objects_to_dictionaries(iterable, fields): + """ + Convert an iterable into a list of dictionaries; fields should beset
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html line 9
Quoted 1 line
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +1
-0+ to a comma-separated string of properties for each item included inthe
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html line 20
Quoted 11 lines
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +11
-0+ resulting list; e.g. for a queryset: + + {{ queryset | objects_to_dictionaries:"id,name" }} + + will return a list like + + [{'id': 1, 'name': 'foo'}, ...] + + providing queryset has id and name fields + + This is mostly to support serialising querysets or lists of modelobjects
Quoted 15 lines
bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +15
-0+ to JSON + """ + objects = [] + + if fields: + fields_list = [field.strip() for field in fields.split(',')] + for item in iterable: + out = {} + for field in fields_list: + out[field] = getattr(item, field) + objects.append(out) + + return objects + +register.filter('objects_to_dictionaries', objects_to_dictionaries)Quoted 1 line
diff --git
a/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.pyQuoted 4 lines
b/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.pydeleted file mode 100644
index dfc094b..0000000
---
a/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.pybitbake/lib/toaster/toastergui/views.py eans multiple licenses exist that cover different
Quoted 42 lines
+++
/dev/nullbitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html +0
-26@@ -1,26 +0,0 @@ -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)bitbake/lib/toaster/toastergui/views.py +1
-0@@ -507,6 +507,7 @@ def builddashboard( request, build_id ): context = { 'build' : build, + 'project' : build.project, 'hasImages' : hasImages, 'ntargets' : ntargets, 'targets' : targets,@@ -797,6 +798,7 @@ eans multiple licenses exist that cover differentparts of the
bitbake/lib/toaster/toastergui/views.py eans multiple licenses exist that cover different parts of the source',
Quoted 6 lines
bitbake/lib/toaster/toastergui/views.py +1
-0source', context = { 'objectname': variant, 'build' : build, + 'project' : build.project, 'target' : Target.objects.filter( pk = target_id)[ 0 ],
bitbake/lib/toaster/toastergui/views.py def dirinfo(request, build_id, target_id,
Quoted 3 lines
bitbake/lib/toaster/toastergui/views.py +0
-0'objects' : packages,'
packages_sum' : packages_sum[ 'installed_size__sum' ],bitbake/lib/toaster/toastergui/views.py +0
-0@@ -937,7 +939,10 @@ def dirinfo(request, build_id, target_id,file_path=None):
bitbake/lib/toaster/toastergui/views.py def tasks_common(request, build_id, variant,
Quoted 12 lines
bitbake/lib/toaster/toastergui/views.py +4
-1if head != sep: dir_list.insert(0, head) - context = { 'build': Build.objects.get(pk=build_id), + build = Build.objects.get(pk=build_id) + + context = { 'build': build, + 'project': build.project, 'target': Target.objects.get(pk=target_id), 'packages_sum': packages_sum['installed_size__sum'], 'objects': objects,@@ -1211,6 +1216,7 @@ def tasks_common(request, build_id, variant,
task_anchor):Quoted 16 lines
bitbake/lib/toaster/toastergui/views.py +2
-0'filter_search_display': filter_search_display, 'mainheading': title_variant, 'build': build, + 'project': build.project, 'objects': task_objects, 'default_orderby' : orderby, 'search_term': search_term,@@ -1282,6 +1288,7 @@ def recipes(request, build_id): context = { 'objectname': 'recipes', 'build': build, + 'project': build.project, 'objects': recipes, 'default_orderby' : 'name:+', 'recipe_deps' : deps,bitbake/lib/toaster/toastergui/views.py def configuration(request, build_id): · line 1373
Quoted 1 line
bitbake/lib/toaster/toastergui/views.py +0
-0@@ -1366,10 +1373,12 @@ def configuration(request, build_id): 'MACHINE', 'DISTRO', 'DISTRO_VERSION','
TUNE_FEATURES', 'TARGET_FPU')Quoted 15 lines
bitbake/lib/toaster/toastergui/views.py +3
-1context = dict(Variable.objects.filter(build=build_id, variable_name__in=var_names)\ .values_list('variable_name', 'variable_value')) + build = Build.objects.get(pk=build_id) context.update({'objectname': 'configuration', 'object_search_display':'variables', 'filter_search_display':'variables', - 'build': Build.objects.get(pk=build_id), + 'build': build, + 'project': build.project, 'targets': Target.objects.filter(build=build_id)})return
render(request, template, context)bitbake/lib/toaster/toastergui/views.py def configvars(request, build_id): · line 1416
Quoted 2 lines
bitbake/lib/toaster/toastergui/views.py +0
-0@@ -1406,12 +1415,15 @@ def configvars(request, build_id): file_filter += '/bitbake.conf'build_dir=re.sub("/tmp/log/.*","",Build.objects.get(pk=build_id).cooker_log_path)
Quoted 23 lines
bitbake/lib/toaster/toastergui/views.py +5
-1+ build = Build.objects.get(pk=build_id) + context = { 'objectname': 'configvars', 'object_search_display':'BitBake variables', 'filter_search_display':'variables', 'file_filter': file_filter, - 'build': Build.objects.get(pk=build_id), + 'build': build, + 'project': build.project, 'objects' : variables, 'total_count':queryset_with_search.count(), 'default_orderby' : 'variable_name:+',@@ -1480,6 +1492,7 @@ def bpackage(request, build_id): context = { 'objectname': 'packages built', 'build': build, + 'project': build.project, 'objects' : packages, 'default_orderby' : 'name:+', 'tablecols':[bitbake/lib/toaster/toastergui/views.py def bpackage(request, build_id): · line 1557
Quoted 4 lines
bitbake/lib/toaster/toastergui/views.py +0
-1@@ -1554,7 +1567,12 @@ def bpackage(request, build_id): def bfile(request, build_id, package_id): template = 'bfile.html' files = Package_File.objects.filter(package = package_id) - context = {'build': Build.objects.get(pk=build_id), 'objects' :files}
Quoted 27 lines
bitbake/lib/toaster/toastergui/views.py +6
-1+ build = Build.objects.get(pk=build_id) + context = { + 'build': build, + 'project': build.project, + 'objects' : files + } return render(request, template, context) --1.9.3
---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). Any review or distribution by others is strictly prohibited. If you are not the intended recipient, please contact the sender and delete all copies. --
_______________________________________________
toaster mailing list
[email protected]
https://lists.yoctoproject.org/listinfo/toaster
Signature · 3 lines
Elliot Smith Software Engineer Intel Open Source Technology Centre
PATCH 3/3 toaster-tests: tests for build dashboard
In reply to Elliot SmithConvert existing tests to Selenium.
Add basic tests to check that the modal contains radio buttons to select a custom image to edit when a build built multiple custom images, and to create a new custom image from one of the images built during the build.
[YOCTO #9123]
--- .../tests/browser/test_builddashboard_page.py | 251 +++++++++++++++++++++ bitbake/lib/toaster/toastergui/tests.py | 87 ------- 2 files changed, 251 insertions(+), 87 deletions(-) create mode 100644 bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
bitbake/lib/toaster/tests/browser/test_builddashboard_page.py new file +251 -0
@@ -0,0 +1,251 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from django.utils import timezone + +from selenium_helpers import SeleniumTestCase + +from orm.models import Project, Release, BitbakeVersion, Build, LogMessage +from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe + +class TestBuildDashboardPage(SeleniumTestCase): + """ Tests for the build dashboard /build/X """ + + def setUp(self): + bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + branch='master', dirpath="") + release = Release.objects.create(name='release1', + bitbake_version=bbv) + project = Project.objects.create_project(name='test project', + release=release) + + now = timezone.now() + + self.build1 = Build.objects.create(project=project, + started_on=now, + completed_on=now) + + self.build2 = Build.objects.create(project=project, + started_on=now, + completed_on=now) + + # exception + msg1 = 'an exception was thrown' + self.exception_message = LogMessage.objects.create( + build=self.build1, + level=LogMessage.EXCEPTION, + message=msg1 + ) + + # critical + msg2 = 'a critical error occurred' + self.critical_message = LogMessage.objects.create( + build=self.build1, + level=LogMessage.CRITICAL, + message=msg2 + ) + + # recipes related to the build, for testing the edit custom image/new + # custom image buttons + layer = Layer.objects.create(name='alayer') + layer_version = Layer_Version.objects.create( + layer=layer, build=self.build1 + ) + + # image recipes + self.image_recipe1 = Recipe.objects.create( + name='recipeA', + layer_version=layer_version, + file_path='/foo/recipeA.bb', + is_image=True + ) + self.image_recipe2 = Recipe.objects.create( + name='recipeB', + layer_version=layer_version, + file_path='/foo/recipeB.bb', + is_image=True + ) + + # custom image recipes for this project + self.custom_image_recipe1 = CustomImageRecipe.objects.create( + name='customRecipeY', + project=project, + layer_version=layer_version, + file_path='/foo/customRecipeY.bb', + base_recipe=self.image_recipe1, + is_image=True + ) + self.custom_image_recipe2 = CustomImageRecipe.objects.create( + name='customRecipeZ', + project=project, + layer_version=layer_version, + file_path='/foo/customRecipeZ.bb', + base_recipe=self.image_recipe2, + is_image=True + ) + + # custom image recipe for a different project (to test filtering + # of image recipes and custom image recipes is correct: this shouldn't + # show up in either query against self.build1) + self.custom_image_recipe3 = CustomImageRecipe.objects.create( + name='customRecipeOmega', + project=Project.objects.create(name='baz', release=release), + layer_version=Layer_Version.objects.create( + layer=layer, build=self.build2 + ), + file_path='/foo/customRecipeOmega.bb', + base_recipe=self.image_recipe2, + is_image=True + ) + + # another non-image recipe (to test filtering of image recipes and + # custom image recipes is correct: this shouldn't show up in either + # for any build) + self.non_image_recipe = Recipe.objects.create( + name='nonImageRecipe', + layer_version=layer_version, + file_path='/foo/nonImageRecipe.bb', + is_image=False + ) + + def _get_build_dashboard(self, build): + """ + Navigate to the build dashboard for build + """ + url = reverse('builddashboard', args=(build.id,)) + self.get(url) + + def _get_build_dashboard_errors(self, build): + """ + Get a list of HTML fragments representing the errors on the + dashboard for the Build object build + """ + self._get_build_dashboard(build) + return self.find_all('#errors div.alert-error') + + def _check_for_log_message(self, build, log_message): + """ + Check whether the LogMessage instance <log_message> is + represented as an HTML error in the dashboard page for the Build object + build + """ + errors = self._get_build_dashboard_errors(build) + self.assertEqual(len(errors), 2) + + expected_text = log_message.message + expected_id = str(log_message.id) + + found = False + for error in errors: + error_text = error.find_element_by_tag_name('pre').text + text_matches = (error_text == expected_text) + + error_id = error.get_attribute('data-error') + id_matches = (error_id == expected_id) + + if text_matches and id_matches: + found = True + break + + template_vars = (expected_text, error_text, + expected_id, error_id) + assertion_error_msg = 'exception not found as error: ' \ + 'expected text "%s" and got "%s"; ' \ + 'expected ID %s and got %s' % template_vars + self.assertTrue(found, assertion_error_msg) + + def _check_labels_in_modal(self, modal, expected): + """ + Check that the text values of the <label> elements inside + the WebElement modal match the list of text values in expected + """ + # labels containing the radio buttons we're testing for + labels = modal.find_elements_by_tag_name('label') + + # because the label content has the structure + # label text + # <input...> + # we have to regex on its innerHTML, as we can't just retrieve the + # "label text" on its own via the Selenium API + labels_text = sorted(map( + lambda label: label.get_attribute('innerHTML'), labels + )) + + expected = sorted(expected) + + self.assertEqual(len(labels_text), len(expected)) + + for idx, label_text in enumerate(labels_text): + self.assertRegexpMatches(label_text, expected[idx]) + + def test_exceptions_show_as_errors(self): + """ + LogMessages with level EXCEPTION should display in the errors + section of the page + """ + self._check_for_log_message(self.build1, self.exception_message) + + def test_criticals_show_as_errors(self): + """ + LogMessages with level CRITICAL should display in the errors + section of the page + """ + self._check_for_log_message(self.build1, self.critical_message) + + def test_edit_custom_image_button(self): + """ + A build which built two custom images should present a modal which lets + the user choose one of them to edit + """ + self._get_build_dashboard(self.build1) + modal = self.driver.find_element_by_id('edit-custom-image-modal') + + # recipes we expect to see in the edit custom image modal + expected_recipes = [ + self.custom_image_recipe1.name, + self.custom_image_recipe2.name + ] + + self._check_labels_in_modal(modal, expected_recipes) + + def test_new_custom_image_button(self): + """ + Check that a build with multiple images and custom images presents + all of them as options for creating a new custom image from + """ + self._get_build_dashboard(self.build1) + + # click the "new custom image" button, which populates the modal + selector = '[data-role="new-custom-image-trigger"] button' + self.click(selector) + + modal = self.driver.find_element_by_id('new-custom-image-modal') + + # recipes we expect to see in the new custom image modal + expected_recipes = [ + self.image_recipe1.name, + self.image_recipe2.name, + self.custom_image_recipe1.name, + self.custom_image_recipe2.name + ] + + self._check_labels_in_modal(modal, expected_recipes)
bitbake/lib/toaster/toastergui/tests.py +0 -87
90 lines not on this page · raw message
Signature · 12 lines
1.9.3 --------------------------------------------------------------------- Intel Corporation (UK) Limited Registered No. 1134945 (England) Registered Office: Pipers Way, Swindon SN3 1RJ VAT No: 860 2173 47 This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). Any review or distribution by others is strictly prohibited. If you are not the intended recipient, please contact the sender and delete all copies.