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) < [email protected]> wrote: > Hi Elliot, > While reviewing open RRs, I noticed that you transposed the yocto number. > It should be #9123. > Dave > > > -----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. > > > > 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 <[email protected]> > > --- > > bitbake/lib/toaster/orm/models.py | 45 ++++++++------ > > .../lib/toaster/toastergui/static/js/libtoaster.js | 2 + > > .../toastergui/static/js/newcustomimage_modal.js | 7 ++- > > bitbake/lib/toaster/toastergui/templates/base.html | 1 - > > .../toastergui/templates/basebuildpage.html | 62 > +++++++++++--------- > > .../templates/editcustomimage_modal.html | 68 > ++++++++++++++++++---- > > .../templatetags/objects_to_dictionaries_filter.py | 35 +++++++++++ > > .../templatetags/queryset_to_list_filter.py | 26 --------- > > bitbake/lib/toaster/toastergui/views.py | 26 +++++++-- > > 9 files changed, 182 insertions(+), 90 deletions(-) > > create mode 100644 > > > bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py > > delete mode 100644 > > bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.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 > > [email protected] > > https://lists.yoctoproject.org/listinfo/toaster > -- Elliot Smith Software Engineer Intel Open Source Technology Centre
-- _______________________________________________ toaster mailing list [email protected] https://lists.yoctoproject.org/listinfo/toaster
