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

Lerner, Dave dave.lerner at windriver.com
Wed Apr 13 10:11:18 PDT 2016


Hi Elliot,
While reviewing open RRs, I noticed that you transposed the yocto number.  It should be #9123.
Dave

> -----Original Message-----
> From: toaster-bounces at yoctoproject.org [mailto:toaster-bounces at yoctoproject.org] On
> Behalf Of Elliot Smith
> Sent: Monday, April 11, 2016 9:56 AM
> To: toaster at yoctoproject.org
> 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.
> 
> 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

> 
> Signed-off-by: Elliot Smith <elliot.smith at intel.com>
> ---
>  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
> 
> diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
> index c63d631..a146541 100644
> --- a/bitbake/lib/toaster/orm/models.py
> +++ b/bitbake/lib/toaster/orm/models.py
> @@ -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')
> diff --git a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
> b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
> index 8d1d20f..88caaff 100644
> --- a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
> +++ b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
> @@ -344,6 +344,8 @@ var libtoaster = (function (){
>    }
> 
>    function _createCustomRecipe(name, baseRecipeId, doneCb){
> +    debugger;
> +
>      var data = {
>        'name' : name,
>        'project' : libtoaster.ctx.projectId,
> diff --git a/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
> b/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
> index 1ae0d34..a6d5b1a 100644
> --- a/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
> +++ b/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
> @@ -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);
> diff --git a/bitbake/lib/toaster/toastergui/templates/base.html
> b/bitbake/lib/toaster/toastergui/templates/base.html
> index 192f9fb..210cf33 100644
> --- a/bitbake/lib/toaster/toastergui/templates/base.html
> +++ b/bitbake/lib/toaster/toastergui/templates/base.html
> @@ -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}},
> diff --git a/bitbake/lib/toaster/toastergui/templates/basebuildpage.html
> b/bitbake/lib/toaster/toastergui/templates/basebuildpage.html
> index 4a8e2a7..0d8c882 100644
> --- a/bitbake/lib/toaster/toastergui/templates/basebuildpage.html
> +++ b/bitbake/lib/toaster/toastergui/templates/basebuildpage.html
> @@ -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 %}';
> +                        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');
> @@ -131,6 +138,7 @@
>                  if (!imageRecipes.length) {
>                    return;
>                  }
> +
>                  newCustomImageModalSetRecipes(imageRecipes);
>                  newCustomImageModal.modal('show');
>                });
> diff --git a/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
> b/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
> index fd998f6..8046c08 100644
> --- a/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
> +++ b/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
> @@ -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>
> -    <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
> --- /dev/null
> +++ b/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py
> @@ -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)
> diff --git a/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py
> 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
> +++ /dev/null
> @@ -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)
> diff --git a/bitbake/lib/toaster/toastergui/views.py
> b/bitbake/lib/toaster/toastergui/views.py
> index 60edb45..1f824ee 100755
> --- a/bitbake/lib/toaster/toastergui/views.py
> +++ b/bitbake/lib/toaster/toastergui/views.py
> @@ -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)
> 
> 
> --
> 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
> toaster at yoctoproject.org
> https://lists.yoctoproject.org/listinfo/toaster


More information about the toaster mailing list