From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: by yocto-www.yoctoproject.org (Postfix, from userid 118) id 05BCFE00F8C; Tue, 19 Apr 2016 09:34:04 -0700 (PDT) X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on yocto-www.yoctoproject.org X-Spam-Level: X-Spam-Status: No, score=-2.6 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,RCVD_IN_DNSWL_LOW autolearn=ham version=3.3.1 X-Spam-HAM-Report: * -0.7 RCVD_IN_DNSWL_LOW RBL: Sender listed at http://www.dnswl.org/, low * trust * [74.125.82.52 listed in list.dnswl.org] * -1.9 BAYES_00 BODY: Bayes spam probability is 0 to 1% * [score: 0.0000] * 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily * valid * -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature Received: from mail-wm0-f52.google.com (mail-wm0-f52.google.com [74.125.82.52]) by yocto-www.yoctoproject.org (Postfix) with ESMTP id C5E2CE00F87 for ; Tue, 19 Apr 2016 09:34:01 -0700 (PDT) Received: by mail-wm0-f52.google.com with SMTP id e201so20669211wme.0 for ; Tue, 19 Apr 2016 09:34:01 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=intel-com.20150623.gappssmtp.com; s=20150623; h=subject:to:references:from:message-id:date:user-agent:mime-version :in-reply-to:content-transfer-encoding; bh=L1kqOrutrXAG7dIcw3EcEQwu3akfAkWVHpn424u0z0g=; b=X/5CTUwPPx8EkalOdxNTJvemCiX5jS8ZDdRfUqXRH5I7RnFEg6UL/lREZ8YMa2Yo/f bPSjSOhk9g7E14/K+bpvqpNzJ4HDEvTzjH749sXJSrCK+nOKjx1IIurOb8qJvnjbrEBu 1BTbqialUPcS5XHhDOlC/RZ7UzFyFAHLxarRT15EzEVdbaaNws4n/+osT9EWi175DSiN BGxAH2WxH7xjXkKB18QpvESnBFEkIs6Hm4mlDXYKckyy69aCUP2EvNXxsKk0d5KzG98U bQQJfgxXA8uJbAHxWDDUgCCwdMsAg3NOpe6oV2WJT90cXDLj69MvcCgTKfoEqSxree+f T5Ag== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:subject:to:references:from:message-id:date :user-agent:mime-version:in-reply-to:content-transfer-encoding; bh=L1kqOrutrXAG7dIcw3EcEQwu3akfAkWVHpn424u0z0g=; b=VMisXlWwcb3Re6lX2s/SE17cNVLmtUm7K9KefnpLGsyNgHSkhNz1EeR2+h6APApBlk P//36IEUOB5Fi3ikL4wKjv/BZFnQLNrqyaDwPkIGChtMmATJvTl290F3pwwtDUCZ0vLv GDBQMy96YWv2Bf4OU7qjFz2bObMKs9+R7Z3erL9yJ7VtSDGvX7IWFX8PMIUeaf29VgLa qnyPvnBI84f4fW+uiFfYMiRvrorfyNt84M7cUUyM9q9ekPz+aYwtjXRWatEiJijGtvr1 f1z+D2Qu2NgraSfoeAYcSDToFcACT3Y1Vf7mIFy/8PCMsRRaZfCPZQ/cBNvSWDdNR2bI JBmA== X-Gm-Message-State: AOPr4FWV83dIQfCZ95eCgtcO+Oedrig2XtFh8s22d+xCiBVQEg8g82D6MkWVeR0IfAxQ/KkD X-Received: by 10.28.24.195 with SMTP id 186mr4620163wmy.30.1461083641061; Tue, 19 Apr 2016 09:34:01 -0700 (PDT) Received: from [192.168.0.83] (host109-150-176-203.range109-150.btcentralplus.com. [109.150.176.203]) by smtp.googlemail.com with ESMTPSA id l124sm5142720wmf.11.2016.04.19.09.34.00 for (version=TLSv1/SSLv3 cipher=OTHER); Tue, 19 Apr 2016 09:34:00 -0700 (PDT) To: toaster@yoctoproject.org References: <9327144bb347ff7e56382b1beb5afae8ed2f0f87.1460386171.git.elliot.smith@intel.com> From: Michael Wood Message-ID: <57165DF7.6090100@intel.com> Date: Tue, 19 Apr 2016 17:33:59 +0100 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Thunderbird/38.6.0 MIME-Version: 1.0 In-Reply-To: <9327144bb347ff7e56382b1beb5afae8ed2f0f87.1460386171.git.elliot.smith@intel.com> Subject: Re: [PATCH 1/3] toaster: add build dashboard buttons to edit/create custom images X-BeenThere: toaster@yoctoproject.org X-Mailman-Version: 2.1.13 Precedence: list List-Id: Web based interface for BitBake List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 19 Apr 2016 16:34:04 -0000 Content-Type: text/plain; charset=utf-8; format=flowed Content-Transfer-Encoding: 8bit Sent to bitbake-devel and added to toaster-next Thanks, Michael On 11/04/16 15:56, Elliot Smith wrote: > When a build is viewed in the dashboard, enable users to edit > a custom image which was built during that build, and/or create > a new custom image based on one of the image recipes built during > the build. > > Add methods to the Build model to enable querying for the > set of image recipes built during a build. > > Add buttons to the dashboard, with the "Edit custom image" > button opening a basic modal for now. The "New custom image" > button opens the existing new custom image modal, but is modified > to show a list of images available as a base for a new custom image. > > Add a new function to the new custom image modal's script which > enables multiple potential custom images to be shown as radio > buttons in the dialog (if there is more than 1). Modify existing > code to use this new function. > > Add a template filter which allows the queryset of recipes for > a build to be available to client-side scripts, and from there > be used to populate the new custom image modal. > > [YOCTO #9123] > > Signed-off-by: Elliot Smith > --- > bitbake/lib/toaster/orm/models.py | 41 ++++ > .../lib/toaster/toastergui/static/js/layerBtn.js | 3 +- > .../toastergui/static/js/newcustomimage_modal.js | 97 +++++++++- > .../toaster/toastergui/static/js/recipedetails.js | 3 +- > .../toastergui/templates/basebuildpage.html | 207 +++++++++++++-------- > .../templates/editcustomimage_modal.html | 23 +++ > .../toastergui/templates/newcustomimage_modal.html | 28 ++- > .../templatetags/queryset_to_list_filter.py | 26 +++ > bitbake/lib/toaster/toastergui/views.py | 7 +- > 9 files changed, 344 insertions(+), 91 deletions(-) > create mode 100644 bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html > create 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 68c3072..c63d631 100644 > --- a/bitbake/lib/toaster/orm/models.py > +++ b/bitbake/lib/toaster/orm/models.py > @@ -484,6 +484,47 @@ class Build(models.Model): > tgts = Target.objects.filter(build_id = self.id).order_by( 'target' ); > return( tgts ); > > + def get_recipes(self): > + """ > + Get the recipes related to this build; > + note that the related layer versions and layers are also prefetched > + by this query, as this queryset can be sorted by these objects in the > + build recipes view; prefetching them here removes the need > + for another query in that view > + """ > + layer_versions = Layer_Version.objects.filter(build=self) > + criteria = Q(layer_version__id__in=layer_versions) > + 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 > + """ > + criteria = Q(is_image=True) > + return self.get_recipes().filter(criteria).order_by('name') > + > + def get_custom_image_recipes(self): > + """ > + Returns a queryset of custom image recipes 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') > + > def get_outcome_text(self): > return Build.BUILD_OUTCOME[int(self.outcome)][1] > > diff --git a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js > index aa43284..259271d 100644 > --- a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js > +++ b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js > @@ -76,7 +76,8 @@ function layerBtnsInit() { > if (imgCustomModal.length == 0) > throw("Modal new-custom-image not found"); > > - imgCustomModal.data('recipe', $(this).data('recipe')); > + var recipe = {id: $(this).data('recipe'), name: null} > + newCustomImageModalSetRecipes([recipe]); > imgCustomModal.modal('show'); > }); > } > diff --git a/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js b/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js > index 328997a..1ae0d34 100644 > --- a/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js > +++ b/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js > @@ -1,29 +1,59 @@ > "use strict"; > > -/* Used for the newcustomimage_modal actions */ > +/* > +Used for the newcustomimage_modal actions > + > +The .data('recipe') value on the outer element determines which > +recipe ID is used as the basis for the new custom image recipe created via > +this modal. > + > +Use newCustomImageModalSetRecipes() to set the recipes available as a base > +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"); > var imgCustomModal = $("#new-custom-image-modal"); > var invalidNameHelp = $("#invalid-name-help"); > + var invalidRecipeHelp = $("#invalid-recipe-help"); > var nameInput = imgCustomModal.find('input'); > > - var invalidMsg = "Image names cannot contain spaces or capital letters. The only allowed special character is dash (-)."; > + var invalidNameMsg = "Image names cannot contain spaces or capital letters. The only allowed special character is dash (-)."; > + var duplicateNameMsg = "An image with this name already exists. Image names must be unique."; > + var invalidBaseRecipeIdMsg = "Please select an image to customise."; > + > + // capture clicks on radio buttons inside the modal; when one is selected, > + // set the recipe on the modal > + imgCustomModal.on("click", "[name='select-image']", function (e) { > + clearRecipeError(); > + > + var recipeId = $(e.target).attr('data-recipe'); > + imgCustomModal.data('recipe', recipeId); > + }); > > newCustomImgBtn.click(function(e){ > e.preventDefault(); > > var baseRecipeId = imgCustomModal.data('recipe'); > > + if (!baseRecipeId) { > + showRecipeError(invalidBaseRecipeIdMsg); > + return; > + } > + > if (nameInput.val().length > 0) { > libtoaster.createCustomRecipe(nameInput.val(), baseRecipeId, > function(ret) { > if (ret.error !== "ok") { > console.warn(ret.error); > if (ret.error === "invalid-name") { > - showError(invalidMsg); > + showNameError(invalidNameMsg); > + return; > } else if (ret.error === "already-exists") { > - showError("An image with this name already exists. Image names must be unique."); > + showNameError(duplicateNameMsg); > + return; > } > } else { > imgCustomModal.modal('hide'); > @@ -33,12 +63,21 @@ function newCustomImageModalInit(){ > } > }); > > - function showError(text){ > + function showNameError(text){ > invalidNameHelp.text(text); > invalidNameHelp.show(); > nameInput.parent().addClass('error'); > } > > + function showRecipeError(text){ > + invalidRecipeHelp.text(text); > + invalidRecipeHelp.show(); > + } > + > + function clearRecipeError(){ > + invalidRecipeHelp.hide(); > + } > + > nameInput.on('keyup', function(){ > if (nameInput.val().length === 0){ > newCustomImgBtn.prop("disabled", true); > @@ -46,7 +85,7 @@ function newCustomImageModalInit(){ > } > > if (nameInput.val().search(/[^a-z|0-9|-]/) != -1){ > - showError(invalidMsg); > + showNameError(invalidNameMsg); > newCustomImgBtn.prop("disabled", true); > nameInput.parent().addClass('error'); > } else { > @@ -56,3 +95,49 @@ function newCustomImageModalInit(){ > } > }); > } > + > +// Set the image recipes which can used as the basis for the custom > +// image recipe the user is creating > +// > +// baseRecipes: a list of one or more recipes which can be > +// used as the base for the new custom image recipe in the format: > +// [{'id': , 'name': '}, ...] > +// > +// if recipes is a single recipe, just show the text box to set the > +// name for the new custom image; if recipes contains multiple recipe objects, > +// show a set of radio buttons so the user can decide which to use as the > +// basis for the new custom image > +function newCustomImageModalSetRecipes(baseRecipes) { > + var imgCustomModal = $("#new-custom-image-modal"); > + var imageSelector = $('#new-custom-image-modal [data-role="image-selector"]'); > + var imageSelectRadiosContainer = $('#new-custom-image-modal [data-role="image-selector-radios"]'); > + > + 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); > + } > + else { > + // add radio buttons; note that the handlers for the radio buttons > + // are set in newCustomImageModalInit via event delegation > + for (var i = 0; i < baseRecipes.length; i++) { > + var recipe = baseRecipes[i]; > + imageSelectRadiosContainer.append( > + '' > + ); > + } > + > + // show the radio button container > + imageSelector.show(); > + } > +} > diff --git a/bitbake/lib/toaster/toastergui/static/js/recipedetails.js b/bitbake/lib/toaster/toastergui/static/js/recipedetails.js > index d5f9eac..604db5f 100644 > --- a/bitbake/lib/toaster/toastergui/static/js/recipedetails.js > +++ b/bitbake/lib/toaster/toastergui/static/js/recipedetails.js > @@ -9,7 +9,8 @@ function recipeDetailsPageInit(ctx){ > if (imgCustomModal.length === 0) > throw("Modal new-custom-image not found"); > > - imgCustomModal.data('recipe', $(this).data('recipe')); > + var recipe = {id: $(this).data('recipe'), name: null} > + newCustomImageModalSetRecipes([recipe]); > imgCustomModal.modal('show'); > }); > > diff --git a/bitbake/lib/toaster/toastergui/templates/basebuildpage.html b/bitbake/lib/toaster/toastergui/templates/basebuildpage.html > index ff9433e..4a8e2a7 100644 > --- a/bitbake/lib/toaster/toastergui/templates/basebuildpage.html > +++ b/bitbake/lib/toaster/toastergui/templates/basebuildpage.html > @@ -1,90 +1,149 @@ > {% extends "base.html" %} > {% load projecttags %} > {% load project_url_tag %} > +{% load queryset_to_list_filter %} > {% load humanize %} > {% block pagecontent %} > + > +
> + > + > +
> + > +
> + > + > + > + > + > + {% block buildinfomain %}{% endblock %} > + > +
> +{% endblock %} > diff --git a/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html b/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html > new file mode 100644 > index 0000000..fd998f6 > --- /dev/null > +++ b/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html > @@ -0,0 +1,23 @@ > + > + > diff --git a/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html b/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html > index b1b5148..caeb302 100644 > --- a/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html > +++ b/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html > @@ -15,18 +15,34 @@ > > + > + > > diff --git a/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py b/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py > new file mode 100644 > index 0000000..dfc094b > --- /dev/null > +++ b/bitbake/lib/toaster/toastergui/templatetags/queryset_to_list_filter.py > @@ -0,0 +1,26 @@ > +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 30295a7..60edb45 100755 > --- a/bitbake/lib/toaster/toastergui/views.py > +++ b/bitbake/lib/toaster/toastergui/views.py > @@ -1257,7 +1257,10 @@ def recipes(request, build_id): > if retval: > return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id) > (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe) > - queryset = Recipe.objects.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id)).select_related("layer_version", "layer_version__layer") > + > + build = Build.objects.get(pk=build_id) > + > + queryset = build.get_recipes() > queryset = _get_queryset(Recipe, queryset, filter_string, search_term, ordering_string, 'name') > > recipes = _build_page_range(Paginator(queryset, pagesize),request.GET.get('page', 1)) > @@ -1276,8 +1279,6 @@ def recipes(request, build_id): > revlist.append(recipe_dep) > revs[recipe.id] = revlist > > - build = Build.objects.get(pk=build_id) > - > context = { > 'objectname': 'recipes', > 'build': build,