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