Skip to contentSkip to conversation

Add buttons to build dashboard to customise/edit images

PATCH In review Elliot Smith 3 participants 7 messages Apr 11, 2016 – Apr 19, 2016

lore
Share & tools
lore
#0 lore raw

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.

#1 lore raw

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
index 68c3072..c63d631 100644
@@ -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
index aa43284..259271d 100644
@@ -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
index d5f9eac..604db5f 100644
@@ -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
new file mode 100644
index 0000000..fd998f6
@@ -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
index b1b5148..caeb302 100644
@@ -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
new file mode 100644
index 0000000..dfc094b
@@ -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
index 30295a7..60edb45 100755
@@ -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.
#2 lore raw

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]

---
  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
index 68c3072..c63d631 100644
@@ -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
index aa43284..259271d 100644
@@ -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
index 328997a..1ae0d34 100644
@@ -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
index d5f9eac..604db5f 100644
@@ -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
index ff9433e..4a8e2a7 100644
@@ -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">&rarr;</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">&rarr;</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
new file mode 100644
index 0000000..fd998f6
@@ -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
index b1b5148..caeb302 100644
@@ -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
new file mode 100644
index 0000000..dfc094b
@@ -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
index 30295a7..60edb45 100755
@@ -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,
#3 lore raw

PATCH 2/3 toaster: add modal to select custom image for editing

In reply to Elliot Smith

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.

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
index c63d631..a146541 100644
@@ -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
index 8d1d20f..88caaff 100644
@@ -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
index 1ae0d34..a6d5b1a 100644
@@ -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
index 192f9fb..210cf33 100644
@@ -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
new file mode 100644
index 0000000..0dcc7d2
@@ -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
deleted file mode 100644
index dfc094b..0000000
@@ -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
index 60edb45..1f824ee 100755
@@ -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.
#4 lore raw

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 editing

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_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]

Bug 9213 - Enable thumb for ARM builds
Bug 9123 - Build history pages are missing the image customisation links

Quoted 544 lines
---
 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
index c63d631..a146541 100644
@@ -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
index 8d1d20f..88caaff 100644
@@ -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
index 1ae0d34..a6d5b1a 100644
@@ -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).
*/

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 from
     imgCustomModal.data('recipe', baseRecipes[0].id);
bitbake/lib/toaster/toastergui/templates/base.html +0 -1
index 192f9fb..210cf33 100644
@@ -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
index 4a8e2a7..0d8c882 100644
@@ -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.id

custom_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
index fd998f6..8046c08 100644
@@ -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.py b/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py new file mode 100644
index 0000000..0dcc7d2

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
deleted file mode 100644
index dfc094b..0000000
@@ -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
index 60edb45..1f824ee 100755
@@ -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 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. --
_______________________________________________
toaster mailing list
[email protected]
https://lists.yoctoproject.org/listinfo/toaster

#5 lore raw

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.
Dave

Quoted 2 lines

-----Original Message-----
From: [email protected] [mailto:

[email protected]] On

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 image

for 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_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]

Bug 9213 - Enable thumb for ARM builds
Bug 9123 - Build history pages are missing the image customisation links

Quoted 8 lines
---
 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.py

Review

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.py

bitbake/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 from

the

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 recipes

from

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) related

to 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 two

steps, 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 image

recipes

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 -1
     def get_custom_image_recipes(self):
         """
-        Returns a queryset of custom image recipes related to this

build,

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
index 8d1d20f..88caaff 100644
@@ -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 the

addition of radio

Quoted 2 lines

buttons
to select the base image (or remove the radio buttons, if there is only

a

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 @@ function

newCustomImageModalSetRecipes(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 -0
   var 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 -0
   var imageSelectRadiosContainer = $('#new-custom-image-modal

[data-role="image-

Quoted 21 lines
bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js +3 -3
 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 -0
index 192f9fb..210cf33 100644

bitbake/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.id

as

bitbake/lib/toaster/toastergui/templates/base.html line 47

Quoted 4 lines
bitbake/lib/toaster/toastergui/templates/base.html +0 -0
 paturl%}{{paturl|json}},
         layersTypeAheadUrl: {% url 'xhr_layerstypeahead' project.id as
 paturl%}{{paturl|json}},
         machinesTypeAheadUrl: {% url 'xhr_machinestypeahead' project.id

as

Quoted 3 lines
bitbake/lib/toaster/toastergui/templates/base.html +0 -1
 paturl%}{{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
index 4a8e2a7..0d8c882 100644
@@ -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 -4
 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-

bitbake/lib/toaster/toastergui/templates/basebuildpage.html line 86

Quoted 2 lines
bitbake/lib/toaster/toastergui/templates/basebuildpage.html +1 -0
 trigger"]');
+        {% 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 -2
 objects_to_dictionaries:"id,name" | json }};
 
-              editCustomImageTrigger.click(function () {
-                // if there is a single editable custom image, go

direct 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 custom

images, 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 -0
 trigger"]');
+                    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 this

build

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 the

edit 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 to

select

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 and

built-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 for

a

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 existing

custom 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 existing

custom 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">Error

text</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.py

Quoted 1 line

b/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py

Quoted 4 lines

new file mode 100644
index 0000000..0dcc7d2
--- /dev/null
+++

b/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py

bitbake/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 be

set

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 in

the

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 model

objects

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.py

Quoted 4 lines

b/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py deleted file mode 100644
index dfc094b..0000000
---

a/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py

bitbake/lib/toaster/toastergui/views.py eans multiple licenses exist that cover different

Quoted 42 lines

+++ /dev/null

bitbake/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
index 60edb45..1f824ee 100755
@@ -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

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 -0
 source',
     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 -1
             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):

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 -1
     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 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 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. --
_______________________________________________
toaster mailing list
[email protected]
https://lists.yoctoproject.org/listinfo/toaster

Signature · 3 lines
Elliot Smith
Software Engineer
Intel Open Source Technology Centre
#6 lore raw

PATCH 3/3 toaster-tests: tests for build dashboard

In reply to Elliot Smith

Convert 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
new file mode 100644
index 0000000..5e08749
@@ -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.