[Toaster] [PATCH 2/3] toaster: add modal to select custom image for editing
Smith, Elliot
elliot.smith at intel.com
Thu Apr 14 00:27:39 PDT 2016
Thanks Dave, nice spot. I will fix it in the next version I submit for
review.
Elliot
On 13 April 2016 at 18:11, Lerner, David M (Wind River) <
dave.lerner at windriver.com> wrote:
> 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
>
--
Elliot Smith
Software Engineer
Intel Open Source Technology Centre
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.yoctoproject.org/pipermail/toaster/attachments/20160414/abfc7b85/attachment-0001.html>
More information about the toaster
mailing list