diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index 75e6ea3996..0b83b991b9 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -503,33 +503,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]
@@ -1380,6 +1384,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/newcustomimage_modal.js b/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
index 1ae0d34e90..cb9ed4da05 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");
@@ -21,7 +22,8 @@ function newCustomImageModalInit(){
var nameInput = imgCustomModal.find('input');
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 duplicateNameMsg = "A recipe with this name already exists. Image names must be unique.";
+ var duplicateImageInProjectMsg = "An image with this name already exists in this project."
var invalidBaseRecipeIdMsg = "Please select an image to customise.";
// capture clicks on radio buttons inside the modal; when one is selected,
@@ -51,9 +53,12 @@ function newCustomImageModalInit(){
if (ret.error === "invalid-name") {
showNameError(invalidNameMsg);
return;
- } else if (ret.error === "already-exists") {
+ } else if (ret.error === "recipe-already-exists") {
showNameError(duplicateNameMsg);
return;
+ } else if (ret.error === "image-already-exists") {
+ showNameError(duplicateImageInProjectMsg);
+ return;
}
} else {
imgCustomModal.modal('hide');
@@ -112,13 +117,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 192f9fb556..210cf3360c 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 4a8e2a7abd..0d8c8820da 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 %}
@@ -81,33 +81,40 @@
-
-
-
-
-
- {% include 'editcustomimage_modal.html' %}
-
-
+ // 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');
+ }
+ });
+ });
+
+
+
+ {% endif %}
+ {% endwith %}
@@ -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 fd998f63eb..8046c08fb5 100644
--- a/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
+++ b/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
@@ -1,23 +1,71 @@
-
Select custom image to edit
+
Which image do you want to edit?
+
-
- Explanation of what this modal is for
-
-
-
-
- Error text
+ {% for recipe in build.get_custom_image_recipes %}
+
+ {% endfor %}
+
+ Please select a custom image to edit.
+
+
+
+
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 0000000000..0dcc7d2714
--- /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 dfc094b591..0000000000
--- 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 942dc31ae9..bd5bf63341 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' : default_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)