[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