diff options
8 files changed, 186 insertions, 92 deletions
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 %} <!-- 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 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 @@ <!-- -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 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) |