[Toaster] [PATCH 1/3] toaster: add build dashboard buttons to edit/create custom images

Michael Wood michael.g.wood at intel.com
Tue Apr 19 09:33:59 PDT 2016


Sent to bitbake-devel and added to toaster-next

Thanks,

Michael

On 11/04/16 15:56, Elliot Smith wrote:
> When a build is viewed in the dashboard, enable users to edit
> a custom image which was built during that build, and/or create
> a new custom image based on one of the image recipes built during
> the build.
>
> Add methods to the Build model to enable querying for the
> set of image recipes built during a build.
>
> Add buttons to the dashboard, with the "Edit custom image"
> button opening a basic modal for now. The "New custom image"
> button opens the existing new custom image modal, but is modified
> to show a list of images available as a base for a new custom image.
>
> Add a new function to the new custom image modal's script which
> enables multiple potential custom images to be shown as radio
> buttons in the dialog (if there is more than 1). Modify existing
> code to use this new function.
>
> Add a template filter which allows the queryset of recipes for
> a build to be available to client-side scripts, and from there
> be used to populate the new custom image modal.
>
> [YOCTO #9123]
>
> Signed-off-by: Elliot Smith <elliot.smith at intel.com>
> ---
>   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
>
> diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
> index 68c3072..c63d631 100644
> --- a/bitbake/lib/toaster/orm/models.py
> +++ b/bitbake/lib/toaster/orm/models.py
> @@ -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]
>   
> diff --git a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
> index aa43284..259271d 100644
> --- a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
> +++ b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
> @@ -76,7 +76,8 @@ function layerBtnsInit() {
>       if (imgCustomModal.length == 0)
>         throw("Modal new-custom-image not found");
>   
> -    imgCustomModal.data('recipe', $(this).data('recipe'));
> +    var recipe = {id: $(this).data('recipe'), name: null}
> +    newCustomImageModalSetRecipes([recipe]);
>       imgCustomModal.modal('show');
>     });
>   }
> diff --git a/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js b/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
> index 328997a..1ae0d34 100644
> --- a/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
> +++ b/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
> @@ -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();
> +  }
> +}
> diff --git a/bitbake/lib/toaster/toastergui/static/js/recipedetails.js b/bitbake/lib/toaster/toastergui/static/js/recipedetails.js
> index d5f9eac..604db5f 100644
> --- a/bitbake/lib/toaster/toastergui/static/js/recipedetails.js
> +++ b/bitbake/lib/toaster/toastergui/static/js/recipedetails.js
> @@ -9,7 +9,8 @@ function recipeDetailsPageInit(ctx){
>       if (imgCustomModal.length === 0)
>         throw("Modal new-custom-image not found");
>   
> -    imgCustomModal.data('recipe', $(this).data('recipe'));
> +    var recipe = {id: $(this).data('recipe'), name: null}
> +    newCustomImageModalSetRecipes([recipe]);
>       imgCustomModal.modal('show');
>     });
>   
> diff --git a/bitbake/lib/toaster/toastergui/templates/basebuildpage.html b/bitbake/lib/toaster/toastergui/templates/basebuildpage.html
> index ff9433e..4a8e2a7 100644
> --- a/bitbake/lib/toaster/toastergui/templates/basebuildpage.html
> +++ b/bitbake/lib/toaster/toastergui/templates/basebuildpage.html
> @@ -1,90 +1,149 @@
>   {% extends "base.html" %}
>   {% load projecttags %}
>   {% load project_url_tag %}
> +{% load queryset_to_list_filter %}
>   {% load humanize %}
>   {% block pagecontent %}
> +  <!-- breadcrumbs -->
> +  <div class="section">
> +    <ul class="breadcrumb" id="breadcrumb">
> +      <li><a href="{% project_url build.project %}">{{build.project.name}}</a></li>
> +      {% if not build.project.is_default %}
> +        <li><a href="{% url 'projectbuilds' build.project.id %}">Builds</a></li>
> +      {% endif %}
> +      <li>
> +        {% block parentbreadcrumb %}
> +          <a href="{%url 'builddashboard' build.pk%}">
> +            {{build.get_sorted_target_list.0.target}} {% if build.target_set.all.count > 1 %}(+{{build.target_set.all.count|add:"-1"}}){% endif %} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}})
> +          </a>
> +        {% endblock %}
> +      </li>
> +      {% block localbreadcrumb %}{% endblock %}
> +    </ul>
> +    <script>
> +      $( function () {
> +        $('#breadcrumb > li').append('<span class="divider">→</span>');
> +        $('#breadcrumb > li:last').addClass("active");
> +        $('#breadcrumb > li:last > span').remove();
> +      });
> +    </script>
> +  </div>
> +
> +  <div class="row-fluid">
> +    <!-- begin left sidebar container -->
> +    <div id="nav" class="span2">
> +      <ul class="nav nav-list well">
> +        <li
> +          {% if request.resolver_match.url_name == 'builddashboard'  %}
> +            class="active"
> +          {% endif %} >
> +          <a class="nav-parent" href="{% url 'builddashboard' build.pk %}">Build summary</a>
> +        </li>
> +        {% if build.target_set.all.0.is_image and build.outcome == 0 %}
> +          <li class="nav-header">Images</li>
> +          {% block nav-target %}
> +            {% for t in build.get_sorted_target_list %}
> +              <li><a href="{% url 'target' build.pk t.pk %}">{{t.target}}</a><li>
> +            {% endfor %}
> +          {% endblock %}
> +        {% endif %}
> +        <li class="nav-header">Build</li>
> +        {% block nav-configuration %}
> +          <li><a href="{% url 'configuration' build.pk %}">Configuration</a></li>
> +        {% endblock %}
> +        {% block nav-tasks %}
> +          <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li>
> +        {% endblock %}
> +        {% block nav-recipes %}
> +          <li><a href="{% url 'recipes' build.pk %}">Recipes</a></li>
> +        {% endblock %}
> +        {% block nav-packages %}
> +          <li><a href="{% url 'packages' build.pk %}">Packages</a></li>
> +        {% endblock %}
> +          <li class="nav-header">Performance</li>
> +        {% block nav-buildtime %}
> +          <li><a href="{% url 'buildtime' build.pk %}">Time</a></li>
> +        {% endblock %}
> +        {% block nav-cputime %}
> +          <li><a href="{% url 'cputime' build.pk %}">CPU usage</a></li>
> +        {% endblock %}
> +        {% block nav-diskio %}
> +          <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li>
> +        {% endblock %}
>   
> +        <li class="divider"></li>
>   
> - <div class="">
> -<!-- Breadcrumbs -->
> -    <div class="section">
> -        <ul class="breadcrumb" id="breadcrumb">
> -            <li><a href="{% project_url build.project %}">{{build.project.name}}</a></li>
> -            {% if not build.project.is_default %}
> -                <li><a href="{% url 'projectbuilds' build.project.id %}">Builds</a></li>
> -            {% endif %}
> -            <li>
> -            {% block parentbreadcrumb %}
> -            <a href="{%url 'builddashboard' build.pk%}">
> -              {{build.get_sorted_target_list.0.target}} {%if build.target_set.all.count > 1%}(+{{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}})
> +        <li>
> +          <p class="navbar-btn">
> +            <a class="btn btn-block" href="{% url 'build_artifact' build.id 'cookerlog' build.id %}">
> +              Download build log
>               </a>
> -            {% endblock %}
> -            </li>
> -            {% block localbreadcrumb %}{% endblock %}
> -        </ul>
> -        <script>
> -        $( function () {
> -            $('#breadcrumb > li').append('<span class="divider">→</span>');
> -            $('#breadcrumb > li:last').addClass("active");
> -            $('#breadcrumb > li:last > span').remove();
> -        });
> -        </script>
> -    </div>
> +          </p>
> +        </li>
>   
> -    <div class="row-fluid">
> +        <li>
> +          <!-- edit custom image built during this build -->
> +          <p class="navbar-btn" data-role="edit-custom-image-trigger">
> +            <button class="btn btn-block">Edit custom image</button>
> +          </p>
> +          {% include 'editcustomimage_modal.html' %}
> +          <script>
> +            $(document).ready(function () {
> +              var editableCustomImageRecipes = {{ build.get_custom_image_recipes | queryset_to_list:"id,name" | json }};
>   
> -        <!-- begin left sidebar container -->
> -        <div id="nav" class="span2">
> -            <ul class="nav nav-list well">
> -              <li
> -                {% if request.resolver_match.url_name == 'builddashboard'  %}
> -                  class="active"
> -                {% endif %} >
> -                <a class="nav-parent" href="{% url 'builddashboard' build.pk %}">Build summary</a>
> -              </li>
> -              {% if build.target_set.all.0.is_image and build.outcome == 0 %}
> -                <li class="nav-header">Images</li>
> -                {% block nav-target %}
> -                  {% for t in build.get_sorted_target_list %}
> -                    <li><a href="{% url 'target' build.pk t.pk %}">{{t.target}}</a><li>
> -                  {% endfor %}
> -                {% endblock %}
> -              {% endif %}
> -              <li class="nav-header">Build</li>
> -              {% block nav-configuration %}
> -                  <li><a href="{% url 'configuration' build.pk %}">Configuration</a></li>
> -              {% endblock %}
> -              {% block nav-tasks %}
> -                  <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li>
> -              {% endblock %}
> -              {% block nav-recipes %}
> -                  <li><a href="{% url 'recipes' build.pk %}">Recipes</a></li>
> -              {% endblock %}
> -              {% block nav-packages %}
> -                  <li><a href="{% url 'packages' build.pk %}">Packages</a></li>
> -              {% endblock %}
> -                  <li class="nav-header">Performance</li>
> -              {% block nav-buildtime %}
> -                  <li><a href="{% url 'buildtime' build.pk %}">Time</a></li>
> -              {% endblock %}
> -              {% block nav-cputime %}
> -                  <li><a href="{% url 'cputime' build.pk %}">CPU time</a></li>
> -              {% endblock %}
> -              {% block nav-diskio %}
> -                  <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li>
> -              {% endblock %}
> -            </ul>
> -        </div>
> -        <!-- end left sidebar container -->
> +              // edit custom image which was built during this build
> +              var editCustomImageModal = $('#edit-custom-image-modal');
> +              var editCustomImageTrigger = $('[data-role="edit-custom-image-trigger"]');
>   
> -        <!-- Begin right container -->
> -        {% block buildinfomain %}{% endblock %}
> -        <!-- End right container -->
> +              editCustomImageTrigger.click(function () {
> +                // if there is a single editable custom image, go direct to the edit
> +                // page for it; if there are multiple editable custom images, show
> +                // dialog to select one of them for editing
>   
> +                // single editable custom image
>   
> -    </div>
> -  </div>
> +                // multiple editable custom images
> +                editCustomImageModal.modal('show');
> +              });
> +            });
> +          </script>
> +        </li>
>   
> +        <li>
> +          <!-- new custom image from image recipe in this build -->
> +          <p class="navbar-btn" data-role="new-custom-image-trigger">
> +            <button class="btn btn-block">New custom image</button>
> +          </p>
> +          {% include 'newcustomimage_modal.html' %}
> +          <script>
> +            // imageRecipes includes both custom image recipes and built-in
> +            // image recipes, any of which can be used as the basis for a
> +            // new custom image
> +            var imageRecipes = {{ build.get_image_recipes | queryset_to_list:"id,name" | json }};
>   
> -{% endblock %}
> +            $(document).ready(function () {
> +              var newCustomImageModal = $('#new-custom-image-modal');
> +              var newCustomImageTrigger = $('[data-role="new-custom-image-trigger"]');
>   
> +              // show create new custom image modal to select an image built
> +              // during this build as the basis for the custom recipe
> +              newCustomImageTrigger.click(function () {
> +                if (!imageRecipes.length) {
> +                  return;
> +                }
> +                newCustomImageModalSetRecipes(imageRecipes);
> +                newCustomImageModal.modal('show');
> +              });
> +            });
> +          </script>
> +        </li>
> +      </ul>
> +
> +    </div>
> +    <!-- end left sidebar container -->
> +
> +    <!-- begin right container -->
> +    {% block buildinfomain %}{% endblock %}
> +    <!-- end right container -->
> +  </div>
> +{% endblock %}
> diff --git a/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html b/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
> new file mode 100644
> index 0000000..fd998f6
> --- /dev/null
> +++ b/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
> @@ -0,0 +1,23 @@
> +<!--
> +modal dialog shown on the build dashboard, for editing an existing custom image
> +-->
> +<div class="modal hide fade in" aria-hidden="false" id="edit-custom-image-modal">
> +  <div class="modal-header">
> +    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
> +    <h3>Select custom image to edit</h3>
> +  </div>
> +  <div class="modal-body">
> +    <div class="row-fluid">
> +      <span class="help-block">
> +        Explanation of what this modal is for
> +      </span>
> +    </div>
> +    <div class="control-group controls">
> +      <input type="text" class="huge" placeholder="input box" required>
> +      <span class="help-block error" style="display:none">Error text</span>
> +    </div>
> +  </div>
> +  <div class="modal-footer">
> +    <button class="btn btn-primary btn-large" disabled>Action</button>
> +  </div>
> +</div>
> diff --git a/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html b/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html
> index b1b5148..caeb302 100644
> --- a/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html
> +++ b/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html
> @@ -15,18 +15,34 @@
>   <div class="modal hide fade in" id="new-custom-image-modal" aria-hidden="false">
>     <div class="modal-header">
>       <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
> -    <h3>Name your custom image</h3>
> +    <h3>New custom image</h3>
>     </div>
> +
>     <div class="modal-body">
> +    <!--
> +    this container is visible if there are multiple image recipes which could
> +    be used as a basis for the new custom image; radio buttons are added to it
> +    via newCustomImageModalSetRecipes() as required
> +    -->
> +    <div data-role="image-selector" style="display:none;">
> +      <h4>Which image do you want to customise?</h4>
> +      <div data-role="image-selector-radios"></div>
> +      <span class="help-block error" id="invalid-recipe-help" style="display:none"></span>
> +      <div class="air"></div>
> +    </div>
> +
> +    <h4>Name your custom image</h4>
> +
>       <div class="row-fluid">
>         <span class="help-block span8">Image names must be unique. They should not contain spaces or capital letters, and the only allowed special character is dash (-).<p></p>
>         </span></div>
>       <div class="control-group controls">
>         <input type="text" class="huge" placeholder="Type the custom image name" required>
> -        <span class="help-block error" id="invalid-name-help" style="display:none"></span>
> -      </div>
> -    </div>
> -    <div class="modal-footer">
> -      <button id="create-new-custom-image-btn" class="btn btn-primary btn-large" data-original-title="" title="" disabled>Create custom image</button>
> +      <span class="help-block error" id="invalid-name-help" style="display:none"></span>
>       </div>
> +  </div>
> +
> +  <div class="modal-footer">
> +    <button id="create-new-custom-image-btn" class="btn btn-primary btn-large" data-original-title="" title="" disabled>Create custom image</button>
> +  </div>
>   </div>
> diff --git a/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py b/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py
> new file mode 100644
> index 0000000..dfc094b
> --- /dev/null
> +++ b/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py
> @@ -0,0 +1,26 @@
> +from django import template
> +import json
> +
> +register = template.Library()
> +
> +def queryset_to_list(queryset, fields):
> +    """
> +    Convert a queryset to a list; fields can be set to a comma-separated
> +    string of fields for each record included in the resulting list; if
> +    omitted, all fields are included for each record, e.g.
> +
> +        {{ queryset | queryset_to_list:"id,name" }}
> +
> +    will return a list like
> +
> +        [{'id': 1, 'name': 'foo'}, ...]
> +
> +    (providing queryset has id and name fields)
> +    """
> +    if fields:
> +        fields_list = [field.strip() for field in fields.split(',')]
> +        return list(queryset.values(*fields_list))
> +    else:
> +        return list(queryset.values())
> +
> +register.filter('queryset_to_list', queryset_to_list)
> diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
> index 30295a7..60edb45 100755
> --- a/bitbake/lib/toaster/toastergui/views.py
> +++ b/bitbake/lib/toaster/toastergui/views.py
> @@ -1257,7 +1257,10 @@ def recipes(request, build_id):
>       if retval:
>           return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id)
>       (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe)
> -    queryset = Recipe.objects.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id)).select_related("layer_version", "layer_version__layer")
> +
> +    build = Build.objects.get(pk=build_id)
> +
> +    queryset = build.get_recipes()
>       queryset = _get_queryset(Recipe, queryset, filter_string, search_term, ordering_string, 'name')
>   
>       recipes = _build_page_range(Paginator(queryset, pagesize),request.GET.get('page', 1))
> @@ -1276,8 +1279,6 @@ def recipes(request, build_id):
>               revlist.append(recipe_dep)
>           revs[recipe.id] = revlist
>   
> -    build = Build.objects.get(pk=build_id)
> -
>       context = {
>           'objectname': 'recipes',
>           'build': build,



More information about the toaster mailing list