summaryrefslogtreecommitdiffstats
path: root/yocto-poky/bitbake/lib/toaster/toastergui
diff options
context:
space:
mode:
Diffstat (limited to 'yocto-poky/bitbake/lib/toaster/toastergui')
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/api.py110
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/fixtures/toastergui-unittest-data.xml446
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/css/default.css8
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/css/qunit-1.18.0.css1
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/base.js229
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/customrecipe.js276
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/importlayer.js30
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js8
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerdetails.js17
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/libtoaster.js89
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/mrbsection.js95
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage.js49
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js148
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/projectpage.js39
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js90
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/qunit-1.18.0.js347
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/recipedetails.js51
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/static/js/table.js349
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/tablefilter.py292
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/tables.py1063
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/base.html144
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/basebuilddetailpage.html11
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/basebuildpage.html213
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html3
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/basetable_top.html4
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/builddashboard.html25
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html48
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/builds.html119
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html14
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/customrecipe.html193
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html71
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/importlayer.html2
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html4
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/landing.html10
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/layer_btn.html2
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html8
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/machine_btn.html12
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html108
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html44
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html48
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html50
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/project.html21
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html56
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds.html11
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/projectconf.html186
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/projects-toastertable.html36
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/projects.html92
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/projecttopbar.html34
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe.html28
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/recipedetails.html177
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/snippets/pkg_dependencies_popover.html14
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/snippets/pkg_revdependencies_popover.html14
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/task.html23
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/tasks.html22
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable-filter.html4
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable.html13
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html13
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py35
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/tests.py859
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/urls.py44
-rwxr-xr-xyocto-poky/bitbake/lib/toaster/toastergui/views.py1200
-rw-r--r--yocto-poky/bitbake/lib/toaster/toastergui/widgets.py112
62 files changed, 5274 insertions, 2590 deletions
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/api.py b/yocto-poky/bitbake/lib/toaster/toastergui/api.py
new file mode 100644
index 000000000..42901f750
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/api.py
@@ -0,0 +1,110 @@
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+# Temporary home for the UI's misc API
+
+from orm.models import Project, ProjectTarget, Build
+from bldcontrol.models import BuildRequest
+from bldcontrol import bbcontroller
+from django.http import HttpResponse, JsonResponse
+from django.views.generic import View
+
+
+class XhrBuildRequest(View):
+
+ def get(self, request, *args, **kwargs):
+ return HttpResponse()
+
+ def post(self, request, *args, **kwargs):
+ """
+ Build control
+
+ Entry point: /xhr_buildrequest/<project_id>
+ Method: POST
+
+ Args:
+ id: id of build to change
+ buildCancel = build_request_id ...
+ buildDelete = id ...
+ targets = recipe_name ...
+
+ Returns:
+ {"error": "ok"}
+ or
+ {"error": <error message>}
+ """
+
+ project = Project.objects.get(pk=kwargs['pid'])
+
+ if 'buildCancel' in request.POST:
+ for i in request.POST['buildCancel'].strip().split(" "):
+ try:
+ br = BuildRequest.objects.get(project=project, pk=i)
+
+ try:
+ bbctrl = bbcontroller.BitbakeController(br.environment)
+ bbctrl.forceShutDown()
+ except:
+ # We catch a bunch of exceptions here because
+ # this is where the server has not had time to start up
+ # and the build request or build is in transit between
+ # processes.
+ # We can safely just set the build as cancelled
+ # already as it never got started
+ build = br.build
+ build.outcome = Build.CANCELLED
+ build.save()
+
+ # We now hand over to the buildinfohelper to update the
+ # build state once we've finished cancelling
+ br.state = BuildRequest.REQ_CANCELLING
+ br.save()
+
+ except BuildRequest.DoesNotExist:
+ return JsonResponse({'error':'No such build id %s' % i})
+
+ return JsonResponse({'error': 'ok'})
+
+ if 'buildDelete' in request.POST:
+ for i in request.POST['buildDelete'].strip().split(" "):
+ try:
+ BuildRequest.objects.select_for_update().get(project = project, pk = i, state__lte = BuildRequest.REQ_DELETED).delete()
+ except BuildRequest.DoesNotExist:
+ pass
+ return JsonResponse({'error': 'ok' })
+
+ if 'targets' in request.POST:
+ ProjectTarget.objects.filter(project = project).delete()
+ s = str(request.POST['targets'])
+ for t in s.translate(None, ";%|\"").split(" "):
+ if ":" in t:
+ target, task = t.split(":")
+ else:
+ target = t
+ task = ""
+ ProjectTarget.objects.create(project = project,
+ target = target,
+ task = task)
+ project.schedule_build()
+
+ return JsonResponse({'error': 'ok' })
+
+ response = HttpResponse()
+ response.status_code = 500
+ return response
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/fixtures/toastergui-unittest-data.xml b/yocto-poky/bitbake/lib/toaster/toastergui/fixtures/toastergui-unittest-data.xml
new file mode 100644
index 000000000..cf35be4be
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/fixtures/toastergui-unittest-data.xml
@@ -0,0 +1,446 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="1" model="orm.project">
+ <field type="CharField" name="name">a test project</field>
+ <field type="CharField" name="short_description"></field>
+ <field to="orm.bitbakeversion" name="bitbake_version" rel="ManyToOneRel">1</field>
+ <field to="orm.release" name="release" rel="ManyToOneRel">1</field>
+ <field type="DateTimeField" name="created">2016-02-15T18:46:20.098248+00:00</field>
+ <field type="DateTimeField" name="updated">2016-02-15T18:46:20.098392+00:00</field>
+ <field type="IntegerField" name="user_id"><None></None></field>
+ <field type="BooleanField" name="is_default">False</field>
+ </object>
+ <object pk="2" model="orm.project">
+ <field type="CharField" name="name">z test project</field>
+ <field type="CharField" name="short_description"></field>
+ <field to="orm.bitbakeversion" name="bitbake_version" rel="ManyToOneRel">1</field>
+ <field to="orm.release" name="release" rel="ManyToOneRel">2</field>
+ <field type="DateTimeField" name="created">2016-02-15T18:46:20.107936+00:00</field>
+ <field type="DateTimeField" name="updated">2016-02-15T18:46:20.108066+00:00</field>
+ <field type="IntegerField" name="user_id"><None></None></field>
+ <field type="BooleanField" name="is_default">False</field>
+ </object>
+ <object pk="1" model="orm.ProjectVariable">
+ <field to="orm.project" name="project" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="name">MACHINE</field>
+ <field type="TextField" name="value">qemux86</field>
+ </object>
+ <object pk="2" model="orm.ProjectVariable">
+ <field to="orm.project" name="project" rel="ManyToOneRel">2</field>
+ <field type="CharField" name="name">MACHINE</field>
+ <field type="TextField" name="value">qemux86</field>
+ </object>
+ <object pk="1" model="orm.build">
+ <field to="orm.project" name="project" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="machine">x12</field>
+ <field type="CharField" name="distro"></field>
+ <field type="CharField" name="distro_version"></field>
+ <field type="DateTimeField" name="started_on">2016-02-14T18:46:20.114530+00:00</field>
+ <field type="DateTimeField" name="completed_on">2016-02-14T18:46:20.114530+00:00</field>
+ <field type="IntegerField" name="outcome">0</field>
+ <field type="CharField" name="cooker_log_path"></field>
+ <field type="CharField" name="build_name">a</field>
+ <field type="CharField" name="bitbake_version"></field>
+ </object>
+ <object pk="2" model="orm.build">
+ <field to="orm.project" name="project" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="machine">raspberrypi2</field>
+ <field type="CharField" name="distro"></field>
+ <field type="CharField" name="distro_version"></field>
+ <field type="DateTimeField" name="started_on">2016-02-13T18:46:20.114530+00:00</field>
+ <field type="DateTimeField" name="completed_on">2016-02-13T18:46:20.114530+00:00</field>
+ <field type="IntegerField" name="outcome">0</field>
+ <field type="CharField" name="cooker_log_path"></field>
+ <field type="CharField" name="build_name">b</field>
+ <field type="CharField" name="bitbake_version"></field>
+ </object>
+ <object pk="3" model="orm.build">
+ <field to="orm.project" name="project" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="machine">qemux86</field>
+ <field type="CharField" name="distro"></field>
+ <field type="CharField" name="distro_version"></field>
+ <field type="DateTimeField" name="started_on">2016-02-12T18:46:20.114530+00:00</field>
+ <field type="DateTimeField" name="completed_on">2016-02-12T18:46:20.114530+00:00</field>
+ <field type="IntegerField" name="outcome">1</field>
+ <field type="CharField" name="cooker_log_path"></field>
+ <field type="CharField" name="build_name">c</field>
+ <field type="CharField" name="bitbake_version"></field>
+ </object>
+ <object pk="4" model="orm.build">
+ <field to="orm.project" name="project" rel="ManyToOneRel">2</field>
+ <field type="CharField" name="machine">qemux86</field>
+ <field type="CharField" name="distro"></field>
+ <field type="CharField" name="distro_version"></field>
+ <field type="DateTimeField" name="started_on">2016-02-11T18:46:20.114530+00:00</field>
+ <field type="DateTimeField" name="completed_on">2016-02-11T18:46:20.114530+00:00</field>
+ <field type="IntegerField" name="outcome">0</field>
+ <field type="CharField" name="cooker_log_path"></field>
+ <field type="CharField" name="build_name">d</field>
+ <field type="CharField" name="bitbake_version"></field>
+ </object>
+ <object pk="1" model="orm.target">
+ <field to="orm.build" name="build" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="target">a image recipe</field>
+ <field type="CharField" name="task"><None></None></field>
+ <field type="BooleanField" name="is_image">False</field>
+ <field type="IntegerField" name="image_size">0</field>
+ <field type="CharField" name="license_manifest_path"><None></None></field>
+ </object>
+ <object pk="2" model="orm.target">
+ <field to="orm.build" name="build" rel="ManyToOneRel">2</field>
+ <field type="CharField" name="target">z something</field>
+ <field type="CharField" name="task"><None></None></field>
+ <field type="BooleanField" name="is_image">False</field>
+ <field type="IntegerField" name="image_size">0</field>
+ <field type="CharField" name="license_manifest_path"><None></None></field>
+ </object>
+ <object pk="3" model="orm.target">
+ <field to="orm.build" name="build" rel="ManyToOneRel">3</field>
+ <field type="CharField" name="target">h recipe</field>
+ <field type="CharField" name="task"><None></None></field>
+ <field type="BooleanField" name="is_image">False</field>
+ <field type="IntegerField" name="image_size">0</field>
+ <field type="CharField" name="license_manifest_path"><None></None></field>
+ </object>
+ <object pk="1" model="orm.package">
+ <field to="orm.build" name="build" rel="ManyToOneRel">1</field>
+ <field to="orm.recipe" name="recipe" rel="ManyToOneRel">2</field>
+ <field type="CharField" name="name">b pkg</field>
+ <field type="CharField" name="installed_name"></field>
+ <field type="CharField" name="version"></field>
+ <field type="CharField" name="revision"></field>
+ <field type="TextField" name="summary"></field>
+ <field type="TextField" name="description"></field>
+ <field type="IntegerField" name="size">777</field>
+ <field type="IntegerField" name="installed_size">0</field>
+ <field type="CharField" name="section"></field>
+ <field type="CharField" name="license">a license</field>
+ </object>
+ <object pk="2" model="orm.package">
+ <field to="orm.build" name="build" rel="ManyToOneRel">1</field>
+ <field to="orm.recipe" name="recipe" rel="ManyToOneRel">2</field>
+ <field type="CharField" name="name">f pkg</field>
+ <field type="CharField" name="installed_name"></field>
+ <field type="CharField" name="version"></field>
+ <field type="CharField" name="revision"></field>
+ <field type="TextField" name="summary"></field>
+ <field type="TextField" name="description"></field>
+ <field type="IntegerField" name="size">4</field>
+ <field type="IntegerField" name="installed_size">10</field>
+ <field type="CharField" name="section"></field>
+ <field type="CharField" name="license">z license</field>
+ </object>
+ <object pk="3" model="orm.package">
+ <field to="orm.build" name="build" rel="ManyToOneRel"><None></None></field>
+ <field to="orm.recipe" name="recipe" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="name">a custom image pkg</field>
+ <field type="CharField" name="installed_name"></field>
+ <field type="CharField" name="version"></field>
+ <field type="CharField" name="revision"></field>
+ <field type="TextField" name="summary"></field>
+ <field type="TextField" name="description"></field>
+ <field type="IntegerField" name="size">10</field>
+ <field type="IntegerField" name="installed_size">0</field>
+ <field type="CharField" name="section"></field>
+ <field type="CharField" name="license">h license</field>
+ </object>
+ <object pk="4" model="orm.package">
+ <field to="orm.build" name="build" rel="ManyToOneRel"><None></None></field>
+ <field to="orm.recipe" name="recipe" rel="ManyToOneRel">2</field>
+ <field type="CharField" name="name">z custom image pkg</field>
+ <field type="CharField" name="installed_name"></field>
+ <field type="CharField" name="version"></field>
+ <field type="CharField" name="revision"></field>
+ <field type="TextField" name="summary"></field>
+ <field type="TextField" name="description"></field>
+ <field type="IntegerField" name="size">5</field>
+ <field type="IntegerField" name="installed_size">0</field>
+ <field type="CharField" name="section"></field>
+ <field type="CharField" name="license">z license</field>
+ </object>
+ <object pk="5" model="orm.package">
+ <field to="orm.build" name="build" rel="ManyToOneRel">1</field>
+ <field to="orm.recipe" name="recipe" rel="ManyToOneRel">4</field>
+ <field type="CharField" name="name">a custom image pkg</field>
+ <field type="CharField" name="installed_name"></field>
+ <field type="CharField" name="version"></field>
+ <field type="CharField" name="revision"></field>
+ <field type="TextField" name="summary"></field>
+ <field type="TextField" name="description"></field>
+ <field type="IntegerField" name="size">2</field>
+ <field type="IntegerField" name="installed_size">0</field>
+ <field type="CharField" name="section"></field>
+ <field type="CharField" name="license">h licence</field>
+ </object>
+ <object pk="1" model="orm.target_installed_package">
+ <field to="orm.target" name="target" rel="ManyToOneRel">1</field>
+ <field to="orm.package" name="package" rel="ManyToOneRel">1</field>
+ </object>
+ <object pk="2" model="orm.target_installed_package">
+ <field to="orm.target" name="target" rel="ManyToOneRel">1</field>
+ <field to="orm.package" name="package" rel="ManyToOneRel">2</field>
+ </object>
+ <object pk="3" model="orm.target_installed_package">
+ <field to="orm.target" name="target" rel="ManyToOneRel">1</field>
+ <field to="orm.package" name="package" rel="ManyToOneRel">3</field>
+ </object>
+ <!-- Note that these augment the existing orm.package of the same pk -->
+ <object pk="4" model="orm.customimagepackage">
+ <field to="orm.customimagerecipe" name="recipe_includes" rel="ManyToManyRel"></field>
+ <field to="orm.customimagerecipe" name="recipe_excludes" rel="ManyToManyRel"></field>
+ <field to="orm.customimagerecipe" name="recipe_appends" rel="ManyToManyRel"><object pk="3"></object></field>
+ </object>
+ <object pk="5" model="orm.customimagepackage">
+ <field to="orm.customimagerecipe" name="recipe_includes" rel="ManyToManyRel"></field>
+ <field to="orm.customimagerecipe" name="recipe_excludes" rel="ManyToManyRel"></field>
+ <field to="orm.customimagerecipe" name="recipe_appends" rel="ManyToManyRel"><object pk="3"></object></field>
+ </object>
+ <object pk="1" model="orm.recipe">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel">1</field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field type="CharField" name="name">z recipe</field>
+ <field type="CharField" name="version">5.2</field>
+ <field to="orm.layer_version" name="layer_version" rel="ManyToOneRel">2</field>
+ <field type="TextField" name="summary">z recipe</field>
+ <field type="TextField" name="description">z recipe</field>
+ <field type="CharField" name="section">z section</field>
+ <field type="CharField" name="license">z license</field>
+ <field type="CharField" name="homepage"></field>
+ <field type="CharField" name="bugtracker"></field>
+ <field type="FilePathField" name="file_path"></field>
+ <field type="CharField" name="pathflags"></field>
+ <field type="BooleanField" name="is_image">False</field>
+ </object>
+ <object pk="2" model="orm.recipe">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel">1</field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field type="CharField" name="name">a recipe</field>
+ <field type="CharField" name="version">1.2</field>
+ <field to="orm.layer_version" name="layer_version" rel="ManyToOneRel">1</field>
+ <field type="TextField" name="summary">a recipe</field>
+ <field type="TextField" name="description">a recipe</field>
+ <field type="CharField" name="section">a section</field>
+ <field type="CharField" name="license">a license</field>
+ <field type="CharField" name="homepage"></field>
+ <field type="CharField" name="bugtracker"></field>
+ <field type="FilePathField" name="file_path">a_recipe.bb</field>
+ <field type="CharField" name="pathflags"></field>
+ <field type="BooleanField" name="is_image">False</field>
+ </object>
+ <object pk="3" model="orm.recipe">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel"><None></None></field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field type="CharField" name="name">a custom recipe</field>
+ <field type="CharField" name="version"></field>
+ <field to="orm.layer_version" name="layer_version" rel="ManyToOneRel">2</field>
+ <field type="TextField" name="summary"></field>
+ <field type="TextField" name="description"></field>
+ <field type="CharField" name="section"></field>
+ <field type="CharField" name="license"></field>
+ <field type="CharField" name="homepage"></field>
+ <field type="CharField" name="bugtracker"></field>
+ <field type="FilePathField" name="file_path">custr</field>
+ <field type="CharField" name="pathflags"></field>
+ <field type="BooleanField" name="is_image">False</field>
+ </object>
+ <object pk="4" model="orm.recipe">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel">1</field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field type="CharField" name="name">a image recipe</field>
+ <field type="CharField" name="version">1.2</field>
+ <field to="orm.layer_version" name="layer_version" rel="ManyToOneRel">1</field>
+ <field type="TextField" name="summary">one recipe</field>
+ <field type="TextField" name="description">recipe</field>
+ <field type="CharField" name="section">A</field>
+ <field type="CharField" name="license">A</field>
+ <field type="CharField" name="homepage"></field>
+ <field type="CharField" name="bugtracker"></field>
+ <field type="FilePathField" name="file_path">/one/</field>
+ <field type="CharField" name="pathflags"></field>
+ <field type="BooleanField" name="is_image">True</field>
+ </object>
+ <object pk="5" model="orm.recipe">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel">1</field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field type="CharField" name="name">z image recipe</field>
+ <field type="CharField" name="version">1.3</field>
+ <field to="orm.layer_version" name="layer_version" rel="ManyToOneRel">2</field>
+ <field type="TextField" name="summary">two image recipe</field>
+ <field type="TextField" name="description">recipe two</field>
+ <field type="CharField" name="section">B</field>
+ <field type="CharField" name="license">Z</field>
+ <field type="CharField" name="homepage"></field>
+ <field type="CharField" name="bugtracker"></field>
+ <field type="FilePathField" name="file_path">/two/</field>
+ <field type="CharField" name="pathflags"></field>
+ <field type="BooleanField" name="is_image">True</field>
+ </object>
+ <object pk="6" model="orm.recipe">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel"><None></None></field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field type="CharField" name="name">z custom recipe</field>
+ <field type="CharField" name="version"></field>
+ <field to="orm.layer_version" name="layer_version" rel="ManyToOneRel">2</field>
+ <field type="TextField" name="summary"></field>
+ <field type="TextField" name="description"></field>
+ <field type="CharField" name="section"></field>
+ <field type="CharField" name="license"></field>
+ <field type="CharField" name="homepage"></field>
+ <field type="CharField" name="bugtracker"></field>
+ <field type="FilePathField" name="file_path">zzzz</field>
+ <field type="CharField" name="pathflags"></field>
+ <field type="BooleanField" name="is_image">False</field>
+ </object>
+ <object pk="1" model="orm.machine">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel"><None></None></field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field to="orm.layer_version" name="layer_version" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="name">a machine</field>
+ <field type="CharField" name="description">a machine</field>
+ </object>
+ <object pk="2" model="orm.machine">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel"><None></None></field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field to="orm.layer_version" name="layer_version" rel="ManyToOneRel">2</field>
+ <field type="CharField" name="name">z machine</field>
+ <field type="CharField" name="description">z machine</field>
+ </object>
+ <object pk="3" model="orm.machine">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel"><None></None></field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field to="orm.layer_version" name="layer_version" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="name">g machine</field>
+ <field type="CharField" name="description">g machine</field>
+ </object>
+ <object pk="1" model="orm.layersource">
+ <field type="CharField" name="name">local</field>
+ <field type="IntegerField" name="sourcetype">1</field>
+ <field type="CharField" name="apiurl"></field>
+ </object>
+ <object pk="1" model="orm.bitbakeversion">
+ <field type="CharField" name="name">test bbv</field>
+ <field type="CharField" name="giturl">/tmp/</field>
+ <field type="CharField" name="branch">master</field>
+ <field type="CharField" name="dirpath"></field>
+ </object>
+ <object pk="1" model="orm.release">
+ <field type="CharField" name="name">test release</field>
+ <field type="CharField" name="description"></field>
+ <field to="orm.bitbakeversion" name="bitbake_version" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="branch_name">master</field>
+ <field type="TextField" name="helptext"><None></None></field>
+ </object>
+ <object pk="2" model="orm.release">
+ <field type="CharField" name="name">test release 2</field>
+ <field type="CharField" name="description"></field>
+ <field to="orm.bitbakeversion" name="bitbake_version" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="branch_name">master</field>
+ <field type="TextField" name="helptext"><None></None></field>
+ </object>
+ <object pk="1" model="orm.releaselayersourcepriority">
+ <field to="orm.release" name="release" rel="ManyToOneRel">1</field>
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel">1</field>
+ <field type="IntegerField" name="priority">0</field>
+ </object>
+ <object pk="1" model="orm.branch">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel">1</field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field type="CharField" name="name">master</field>
+ <field type="CharField" name="short_description"></field>
+ </object>
+ <object pk="1" model="orm.layer">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel"><None></None></field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field type="CharField" name="name">a layer</field>
+ <field type="CharField" name="layer_index_url"></field>
+ <field type="CharField" name="vcs_url">/tmp/</field>
+ <field type="CharField" name="vcs_web_url"><None></None></field>
+ <field type="CharField" name="vcs_web_tree_base_url"><None></None></field>
+ <field type="CharField" name="vcs_web_file_base_url"><None></None></field>
+ <field type="TextField" name="summary"><None></None></field>
+ <field type="TextField" name="description"><None></None></field>
+ </object>
+ <object pk="2" model="orm.layer">
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel">1</field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field type="CharField" name="name">z layer</field>
+ <field type="CharField" name="layer_index_url"></field>
+ <field type="CharField" name="vcs_url">git://two/</field>
+ <field type="CharField" name="vcs_web_url"><None></None></field>
+ <field type="CharField" name="vcs_web_tree_base_url"><None></None></field>
+ <field type="CharField" name="vcs_web_file_base_url"><None></None></field>
+ <field type="TextField" name="summary"><None></None></field>
+ <field type="TextField" name="description"><None></None></field>
+ </object>
+ <object pk="1" model="orm.layer_version">
+ <field to="orm.build" name="build" rel="ManyToOneRel"><None></None></field>
+ <field to="orm.layer" name="layer" rel="ManyToOneRel">1</field>
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel">1</field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field to="orm.branch" name="up_branch" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="branch"></field>
+ <field type="CharField" name="commit">master</field>
+ <field type="CharField" name="dirpath">/tmp/</field>
+ <field type="IntegerField" name="priority">0</field>
+ <field type="FilePathField" name="local_path">/</field>
+ <field to="orm.project" name="project" rel="ManyToOneRel">1</field>
+ </object>
+ <object pk="2" model="orm.layer_version">
+ <field to="orm.build" name="build" rel="ManyToOneRel"><None></None></field>
+ <field to="orm.layer" name="layer" rel="ManyToOneRel">2</field>
+ <field to="orm.layersource" name="layer_source" rel="ManyToOneRel">1</field>
+ <field type="IntegerField" name="up_id"><None></None></field>
+ <field type="DateTimeField" name="up_date"><None></None></field>
+ <field to="orm.branch" name="up_branch" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="branch"></field>
+ <field type="CharField" name="commit">master</field>
+ <field type="CharField" name="dirpath"><None></None></field>
+ <field type="IntegerField" name="priority">0</field>
+ <field type="FilePathField" name="local_path">/</field>
+ <field to="orm.project" name="project" rel="ManyToOneRel"><None></None></field>
+ </object>
+ <object pk="1" model="orm.projectlayer">
+ <field to="orm.project" name="project" rel="ManyToOneRel">1</field>
+ <field to="orm.layer_version" name="layercommit" rel="ManyToOneRel">1</field>
+ <field type="BooleanField" name="optional">True</field>
+ </object>
+ <object pk="3" model="orm.customimagerecipe">
+ <field to="orm.recipe" name="base_recipe" rel="ManyToOneRel">2</field>
+ <field to="orm.project" name="project" rel="ManyToOneRel">1</field>
+ <field type="DateTimeField" name="last_updated"><None></None></field>
+ </object>
+ <object pk="6" model="orm.customimagerecipe">
+ <field to="orm.recipe" name="base_recipe" rel="ManyToOneRel">4</field>
+ <field to="orm.project" name="project" rel="ManyToOneRel">1</field>
+ <field type="DateTimeField" name="last_updated"><None></None></field>
+ </object>
+ <object pk="1" model="orm.logmessage">
+ <field to="orm.build" name="build" rel="ManyToOneRel">2</field>
+ <field to="orm.task" name="task" rel="ManyToOneRel"><None></None></field>
+ <field type="IntegerField" name="level">1</field>
+ <field type="TextField" name="message"><None></None></field>
+ <field type="FilePathField" name="pathname"></field>
+ <field type="IntegerField" name="lineno"><None></None></field>
+ </object>
+ <object pk="2" model="orm.logmessage">
+ <field to="orm.build" name="build" rel="ManyToOneRel">2</field>
+ <field to="orm.task" name="task" rel="ManyToOneRel"><None></None></field>
+ <field type="IntegerField" name="level">2</field>
+ <field type="TextField" name="message"><None></None></field>
+ <field type="FilePathField" name="pathname"></field>
+ <field type="IntegerField" name="lineno"><None></None></field>
+ </object>
+</django-objects>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/css/default.css b/yocto-poky/bitbake/lib/toaster/toastergui/static/css/default.css
index bc8a97bed..b024d962a 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/static/css/default.css
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/css/default.css
@@ -10,7 +10,6 @@
/* Style the breadcrumb */
.breadcrumb { display: inline-block; background-color: transparent; }
-.breadcrumb li:first-child { padding-right: 10px; }
/* Styles for the help information */
.get-help { color: #CCCCCC; }
@@ -51,6 +50,7 @@ dd li { line-height: 25px; }
/* Style the filter modal dialogs */
.modal { width: 800px; margin-left: -400px; }
.modal-footer .btn { float: left; }
+.modal-body { max-height: 300px; }
/* Hover style for the clear search icon */
.icon-remove-sign:hover { color: #999999; cursor: pointer; }
@@ -114,6 +114,7 @@ th > a, th > span { font-weight: normal; }
/* Styles for the navbar actions */
.btn-group + .btn-group { margin-right: 10px; }
.navbar-inner > .btn-group { margin-top: 6px; }
+[id^="search-input-"], #search { width: 80%; }
/* Styles for the parent item in the left navigation */
@@ -153,6 +154,9 @@ select { width: auto; }
/* make tables Chrome-happy (me, not so much) */
table { table-layout: fixed; word-wrap: break-word; }
+table p { margin-bottom: 0 }
+.table td { vertical-align: middle; }
+
/* styles for the new build button */
.new-build .btn-primary { padding: 4px 30px; }
.new-build .alert { margin-top: 10px; }
@@ -194,7 +198,7 @@ table { table-layout: fixed; word-wrap: break-word; }
.configuration-alert p { margin-bottom: 0px; }
.project-form { margin-top: 10px; }
.add-layers .btn-block + .btn-block, .build .btn-block + .btn-block { margin-top: 0px; }
-input.huge { font-size: 17.5px; padding: 11px 19px; }
+input.huge { font-size: 17.5px; padding: 11px; min-width: 60%; }
.build-form { margin-bottom: 0px; }
.build-form .input-append { margin-bottom: 0px; }
.build-form .btn-large { padding: 11px 35px; }
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/css/qunit-1.18.0.css b/yocto-poky/bitbake/lib/toaster/toastergui/static/css/qunit-1.18.0.css
new file mode 100644
index 000000000..28cca1cc1
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/css/qunit-1.18.0.css
@@ -0,0 +1 @@
+/**//**/#qunit-tests,#qunit-header,#qunit-banner,#qunit-testrunner-toolbar,#qunit-userAgent,#qunit-testresult{font-family:"Helvetica Neue Light","HelveticaNeue-Light","Helvetica Neue",Calibri,Helvetica,Arial,sans-serif;}#qunit-testrunner-toolbar,#qunit-userAgent,#qunit-testresult,#qunit-tests li{font-size:small;}#qunit-tests{font-size:smaller;}/**/#qunit-tests,#qunit-header,#qunit-banner,#qunit-userAgent,#qunit-testresult,#qunit-modulefilter{margin:0;padding:0;}/**/#qunit-header{padding:0.5em 0 0.5em 1em;color:#8699A4;background-color:#0D3349;font-size:1.5em;line-height:1em;font-weight:400;border-radius:5px 5px 0 0;}#qunit-header a{text-decoration:none;color:#C2CCD1;}#qunit-header a:hover,#qunit-header a:focus{color:#FFF;}#qunit-testrunner-toolbar label{display:inline-block;padding:0 0.5em 0 0.1em;}#qunit-banner{height:5px;}#qunit-testrunner-toolbar{padding:0.5em 1em 0.5em 1em;color:#5E740B;background-color:#EEE;overflow:hidden;}#qunit-userAgent{padding:0.5em 1em 0.5em 1em;background-color:#2B81AF;color:#FFF;text-shadow:rgba(0,0,0,0.5) 2px 2px 1px;}#qunit-modulefilter-container{float:right;padding:0.2em;}.qunit-url-config{display:inline-block;padding:0.1em;}.qunit-filter{display:block;float:right;margin-left:1em;}/**/#qunit-tests{list-style-position:inside;}#qunit-tests li{padding:0.4em 1em 0.4em 1em;border-bottom:1px solid #FFF;list-style-position:inside;}#qunit-tests>li{display:none;}#qunit-tests li.running,#qunit-tests li.pass,#qunit-tests li.fail,#qunit-tests li.skipped{display:list-item;}#qunit-tests.hidepass li.running,#qunit-tests.hidepass li.pass{visibility:hidden;position:absolute;width:0px;height:0px;padding:0;border:0;margin:0;}#qunit-tests li strong{cursor:pointer;}#qunit-tests li.skipped strong{cursor:default;}#qunit-tests li a{padding:0.5em;color:#C2CCD1;text-decoration:none;}#qunit-tests li p a{padding:0.25em;color:#6B6464;}#qunit-tests li a:hover,#qunit-tests li a:focus{color:#000;}#qunit-tests li .runtime{float:right;font-size:smaller;}.qunit-assert-list{margin-top:0.5em;padding:0.5em;background-color:#FFF;border-radius:5px;}.qunit-collapsed{display:none;}#qunit-tests table{border-collapse:collapse;margin-top:0.2em;}#qunit-tests th{text-align:right;vertical-align:top;padding:0 0.5em 0 0;}#qunit-tests td{vertical-align:top;}#qunit-tests pre{margin:0;white-space:pre-wrap;word-wrap:break-word;}#qunit-tests del{background-color:#E0F2BE;color:#374E0C;text-decoration:none;}#qunit-tests ins{background-color:#FFCACA;color:#500;text-decoration:none;}/**/#qunit-tests b.counts{color:#000;}#qunit-tests b.passed{color:#5E740B;}#qunit-tests b.failed{color:#710909;}#qunit-tests li li{padding:5px;background-color:#FFF;border-bottom:none;list-style-position:inside;}/**/#qunit-tests li li.pass{color:#3C510C;background-color:#FFF;border-left:10px solid #C6E746;}#qunit-tests .pass{color:#528CE0;background-color:#D2E0E6;}#qunit-tests .pass .test-name{color:#366097;}#qunit-tests .pass .test-actual,#qunit-tests .pass .test-expected{color:#999;}#qunit-banner.qunit-pass{background-color:#C6E746;}/**/#qunit-tests li li.fail{color:#710909;background-color:#FFF;border-left:10px solid #EE5757;white-space:pre;}#qunit-tests>li:last-child{border-radius:0 0 5px 5px;}#qunit-tests .fail{color:#000;background-color:#EE5757;}#qunit-tests .fail .test-name,#qunit-tests .fail .module-name{color:#000;}#qunit-tests .fail .test-actual{color:#EE5757;}#qunit-tests .fail .test-expected{color:#008000;}#qunit-banner.qunit-fail{background-color:#EE5757;}/**/#qunit-tests .skipped{background-color:#EBECE9;}#qunit-tests .qunit-skipped-label{background-color:#F4FF77;display:inline-block;font-style:normal;color:#366097;line-height:1.8em;padding:0 0.5em;margin:-0.4em 0.4em -0.4em 0;}/**/#qunit-testresult{padding:0.5em 1em 0.5em 1em;color:#2B81AF;background-color:#D2E0E6;border-bottom:1px solid #FFF;}#qunit-testresult .module-name{font-weight:700;}/**/#qunit-fixture{position:absolute;top:-10000px;left:-10000px;width:1000px;height:1000px;} \ No newline at end of file
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/base.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/base.js
deleted file mode 100644
index ed22a4ebc..000000000
--- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/base.js
+++ /dev/null
@@ -1,229 +0,0 @@
-'use strict';
-
-function basePageInit(ctx) {
-
- var newBuildButton = $("#new-build-button");
- var newBuildTargetInput;
- var newBuildTargetBuildBtn;
- var projectNameForm = $("#project-name-change-form");
- var projectNameContainer = $("#project-name-container");
- var projectName = $("#project-name");
- var projectNameFormToggle = $("#project-change-form-toggle");
- var projectNameChangeCancel = $("#project-name-change-cancel");
-
- /* initially the current project is used unless overridden by the new build
- * button in top right nav
- */
- var selectedProject = libtoaster.ctx;
-
- var selectedTarget;
-
- var newBuildProjectInput = $("#new-build-button #project-name-input");
- var newBuildProjectSaveBtn = $("#new-build-button #save-project-button");
-
- /* Project name change functionality */
- projectNameFormToggle.click(function(e){
- e.preventDefault();
- projectNameContainer.hide();
- projectNameForm.fadeIn();
- });
-
- projectNameChangeCancel.click(function(e){
- e.preventDefault();
- projectNameForm.hide();
- projectNameContainer.fadeIn();
- });
-
- $("#project-name-change-btn").click(function(e){
- var newProjectName = $("#project-name-change-input").val();
-
- libtoaster.editCurrentProject({ projectName: newProjectName }, function (){
- projectName.html(newProjectName);
- libtoaster.ctx.projectName = newProjectName;
- projectNameChangeCancel.click();
- });
- });
-
- _checkProjectBuildable();
-
- $("#project-topbar .nav li a").each(function(){
- if (window.location.pathname === $(this).attr('href'))
- $(this).parent().addClass('active');
- else
- $(this).parent().removeClass('active');
- });
-
- if ($(".total-builds").length !== 0){
- libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, function(prjInfo){
- if (prjInfo.completedbuilds)
- $(".total-builds").text(prjInfo.completedbuilds.length);
- });
- }
-
- /* Hide the button if we're on the project,newproject or importlyaer page
- * or if there are no projects yet defined
- * only show if there isn't already a build-target-input already
- */
- if (ctx.numProjects > 0 &&
- ctx.currentUrl.search('newproject') < 0 &&
- $(".build-target-input").length === 1) {
-
- newBuildTargetInput = $("#new-build-button .build-target-input");
- newBuildTargetBuildBtn = $("#new-build-button").find(".build-button");
-
- _setupNewBuildButton();
- newBuildButton.show();
- } else if ($(".build-target-input").length > 0) {
- newBuildTargetInput = $("#project-topbar .build-target-input");
- newBuildTargetBuildBtn = $("#project-topbar .build-button");
- } else {
- return;
- }
-
- /* Hide the change project icon when there is only one project */
- if (ctx.numProjects === 1) {
- $('#project .icon-pencil').hide();
- }
-
- /* If we have a project setup the typeahead */
- if (selectedProject.recipesTypeAheadUrl){
- libtoaster.makeTypeahead(newBuildTargetInput, selectedProject.recipesTypeAheadUrl, { format: "json" }, function (item) {
- selectedTarget = item;
- newBuildTargetBuildBtn.removeAttr("disabled");
- });
- }
-
- newBuildTargetInput.on('input', function () {
- if ($(this).val().length === 0) {
- newBuildTargetBuildBtn.attr("disabled", "disabled");
- } else {
- newBuildTargetBuildBtn.removeAttr("disabled");
- }
- });
-
- newBuildTargetBuildBtn.click(function (e) {
- e.preventDefault();
-
- if (!newBuildTargetInput.val()) {
- return;
- }
-
- /* We use the value of the input field so as to maintain any command also
- * added e.g. core-image-minimal:clean
- */
- selectedTarget = { name: newBuildTargetInput.val() };
-
- /* Fire off the build */
- libtoaster.startABuild(selectedProject.projectBuildsUrl,
- selectedProject.projectId, selectedTarget.name, function(){
- window.location.replace(selectedProject.projectBuildsUrl);
- }, null);
- });
-
- function _checkProjectBuildable() {
- if (selectedProject.projectId === undefined || selectedProject.projectIsDefault) {
- return;
- }
-
- libtoaster.getProjectInfo(selectedProject.projectPageUrl,
- function (data) {
- if (data.machine === null || data.machine.name === undefined || data.layers.length === 0) {
- /* we can't build anything without a machine and some layers */
- $("#new-build-button #targets-form").hide();
- $("#new-build-button .alert").show();
- } else {
- $("#new-build-button #targets-form").show();
- $("#new-build-button .alert").hide();
-
- /* we can build this project; enable input fields */
- newBuildTargetInput.removeAttr("disabled");
- }
- }, null);
- }
-
- /* Setup New build button in the top nav bar */
- function _setupNewBuildButton() {
-
- /* If we don't have a current project then present the set project
- * form.
- */
- if (selectedProject.projectId === undefined || selectedProject.projectIsDefault) {
- $('#change-project-form').show();
- $('#project .icon-pencil').hide();
- }
-
- libtoaster.makeTypeahead(newBuildProjectInput, selectedProject.projectsTypeAheadUrl, { format : "json" }, function (item) {
- /* successfully selected a project */
- newBuildProjectSaveBtn.removeAttr("disabled");
- selectedProject = item;
- });
-
- /* Any typing in the input apart from enter key is going to invalidate
- * the value that has been set by selecting a suggestion from the typeahead
- */
- newBuildProjectInput.on('input', function (event) {
- if (event.keyCode === 13) {
- return;
- }
- newBuildProjectSaveBtn.attr("disabled", "disabled");
- });
-
-
- newBuildProjectSaveBtn.click(function () {
- selectedProject.projectId = selectedProject.id;
- /* Update the typeahead project_id paramater */
- _checkProjectBuildable();
-
- newBuildTargetInput.removeAttr("disabled");
-
- /* We've got a new project so now we need to update the
- * target urls. We can get this from the new project's info
- */
- $.getJSON(selectedProject.projectPageUrl, { format: "json" },
- function(projectInfo){
- /* Update the typeahead to use the new selectedProject */
- selectedProject = projectInfo;
-
- libtoaster.makeTypeahead(newBuildTargetInput, selectedProject.recipesTypeAheadUrl, { format: "json" }, function (item) {
- /* successfully selected a target */
- selectedTarget = item;
- newBuildTargetBuildBtn.removeAttr("disabled");
- });
-
- });
- newBuildTargetInput.val("");
-
- /* set up new form aspect */
- $("#new-build-button #project a").text(selectedProject.name).attr('href', selectedProject.projectPageUrl);
- $("#new-build-button .alert a").attr('href', selectedProject.projectPageUrl);
- $("#project .icon-pencil").show();
-
- $("#change-project-form").slideUp({ 'complete' : function () {
- $("#new-build-button #project").show();
- }});
- });
-
- $('#new-build-button #project .icon-pencil').click(function () {
- newBuildProjectSaveBtn.attr("disabled", "disabled");
- newBuildProjectInput.val($("#new-build-button #project a").text());
- $("#cancel-change-project").show();
- $(this).parent().hide();
- $("#change-project-form").slideDown();
- });
-
- $("#new-build-button #cancel-change-project").click(function () {
- $("#change-project-form").hide(function () {
- $('#new-build-button #project').show();
- });
-
- newBuildProjectInput.val("");
- newBuildProjectSaveBtn.attr("disabled", "disabled");
- });
-
- /* Keep the dropdown open even unless we click outside the dropdown area */
- $(".new-build").click (function (event) {
- event.stopPropagation();
- });
- };
-
-}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/customrecipe.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/customrecipe.js
index 4f6b304dd..1c0ef9e37 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/customrecipe.js
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/customrecipe.js
@@ -3,48 +3,282 @@
function customRecipePageInit(ctx) {
var urlParams = libtoaster.parseUrlParams();
+ var customiseTable = $("#selectpackagestable");
+ var addPkgDepsModalBtn = $("#add-package-deps-modal-btn");
+ var rmdPkgReverseDepsModalBtn = $("#rm-package-reverse-deps-modal-btn");
- (function notificationRequest(){
- if (urlParams.hasOwnProperty('notify') && urlParams.notify === 'new'){
- $("#image-created-notification").show();
- }
- })();
+ if (urlParams.hasOwnProperty('notify') && urlParams.notify === 'new'){
+ $("#image-created-notification").show();
+ }
- $("#recipeselection").on('table-done', function(e, total, tableParams){
+ customiseTable.on('table-done', function(e, total){
/* Table is done so now setup the click handler for the package buttons */
$(".add-rm-package-btn").click(function(e){
e.preventDefault();
- addRemovePackage($(this), tableParams);
+ var targetPkg = $(this).data();
+
+ checkPackageDeps(targetPkg, function(pkgData){
+ if (targetPkg.directive === 'add'){
+ /* If we're adding a package we may need to show the modal to advise
+ * on dependencies for this package.
+ */
+ if (pkgData.unsatisfied_dependencies.length === 0){
+ addRemovePackage(targetPkg);
+ } else {
+ showPackageDepsModal(targetPkg, pkgData);
+ }
+ } else if (targetPkg.directive === 'remove') {
+ if (pkgData.reverse_dependencies.length === 0){
+ addRemovePackage(targetPkg);
+ } else {
+ showPackageReverseDepsModal(targetPkg, pkgData);
+ }
+ }
+ });
+ });
+ });
+
+ function checkPackageDeps(targetPkg, doneCb){
+ $.ajax({
+ type: 'GET',
+ url: targetPkg.packageUrl,
+ headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
+ success: function(data){
+ if (data.error !== 'ok'){
+ console.warn(data.error);
+ return;
+ }
+ doneCb(data);
+ }
});
+ }
+
+ function showPackageDepsModal(targetPkg, pkgData){
+ var modal = $("#package-deps-modal");
+ var depsList = modal.find("#package-add-dep-list");
+ var deps = pkgData.unsatisfied_dependencies;
+
+ modal.find(".package-to-add-name").text(targetPkg.name);
+
+ depsList.text("");
+
+ for (var i in deps){
+ var li = $('<li></li>').text(deps[i].name);
+ li.append($('<span></span>').text(" ("+
+ deps[i].size_formatted+")"));
+ depsList.append(li);
+ }
+
+ modal.find("#package-deps-total-size").text(
+ pkgData.unsatisfied_dependencies_size_formatted);
+
+ targetPkg.depsAdded = deps;
+
+ addPkgDepsModalBtn.data(targetPkg);
+ modal.modal('show');
+ }
+
+ addPkgDepsModalBtn.click(function(e){
+ e.preventDefault();
+
+ addRemovePackage($(this).data(), null);
+ });
+
+ function showPackageReverseDepsModal(targetPkg, pkgData){
+ var modal = $("#package-reverse-deps-modal");
+ var depsList = modal.find("#package-reverse-dep-list");
+ var deps = pkgData.reverse_dependencies;
+
+ var depsCount = deps.length;
+ var vDepends = "depends";
+ var vPackage = "package";
+ var vThis = "this";
+ if (depsCount > 1) {
+ vDepends = "depend";
+ vPackage = "packages";
+ vThis = "these";
+ }
+ modal.find(".package-to-rm-name").text(targetPkg.name);
+ modal.find(".reverse-deps-count").text(depsCount);
+ modal.find(".reverse-deps-count-plus1").text((depsCount+1) + " packages");
+ modal.find(".reverse-deps-depends").text(vDepends);
+ modal.find(".reverse-deps-package").text(vPackage);
+ modal.find(".reverse-deps-this").text(vThis);
+
+ depsList.text("");
+
+ for (var i in deps){
+ var li = $('<li></li>').text(deps[i].name);
+ li.append($('<span></span>').text(" ("+
+ deps[i].size_formatted+")"));
+ depsList.append(li);
+ }
+
+ modal.find("#package-reverse-deps-total-size").text(
+ pkgData.reverse_dependencies_size_formatted);
+
+ targetPkg.depsRemoved = deps;
+
+ rmdPkgReverseDepsModalBtn.data(targetPkg);
+ modal.modal('show');
+ }
+
+ rmdPkgReverseDepsModalBtn.click(function(e){
+ e.preventDefault();
+
+ addRemovePackage($(this).data(), null);
});
- function addRemovePackage(pkgBtn, tableParams){
- var pkgBtnData = pkgBtn.data();
+
+ function addRemovePackage(targetPkg, tableParams){
var method;
- var buttonToShow;
+ var msg = "You have ";
+
+ var btnCell = $("#package-btn-cell-" + targetPkg.id);
+ var inlineNotify = btnCell.children(".inline-notification");
- if (pkgBtnData.directive == 'add') {
+ var i;
+ var dep;
+ var depBtnCell;
+
+ if (targetPkg.directive === 'add') {
method = 'PUT';
- buttonToShow = '#package-rm-btn-' + pkgBtnData.package;
- } else if (pkgBtnData.directive == 'remove') {
+ /* If the package had dependencies also notify that they were added */
+ if (targetPkg.hasOwnProperty('depsAdded') &&
+ targetPkg.depsAdded.length > 0) {
+
+ msg += "added ";
+ msg += "<strong>" + (targetPkg.depsAdded.length + 1) + "</strong>";
+ msg += " packages to " + ctx.recipe.name + ": ";
+ msg += "<strong>" + targetPkg.name + "</strong> and its dependencies";
+
+ for (i in targetPkg.depsAdded){
+ dep = targetPkg.depsAdded[i];
+
+ msg += " <strong>" + dep.name + "</strong>";
+
+ /* Add any cells currently in view to the list of cells which get
+ * an inline notification inside them and which change add/rm state
+ */
+ depBtnCell = $("#package-btn-cell-" + dep.pk);
+ btnCell = btnCell.add(depBtnCell);
+
+ inlineNotify = inlineNotify.add(
+ depBtnCell.children(".inline-notification"));
+ }
+
+ inlineNotify.text(
+ (targetPkg.depsAdded.length + 1) + " packages added");
+
+ } else {
+ msg += "added <strong>1</strong>";
+ msg += " package to " + ctx.recipe.name + ": ";
+ msg += "<strong>" + targetPkg.name + "</strong>";
+ inlineNotify.text("1 package added");
+ }
+
+ } else if (targetPkg.directive === 'remove') {
method = 'DELETE';
- buttonToShow = '#package-add-btn-' + pkgBtnData.package;
+ var numPackageString = "1 package ";
+ var revDepList = "";
+ if (targetPkg.hasOwnProperty('depsRemoved') &&
+ targetPkg.depsRemoved.length > 0) {
+ var depsRemovedLength = targetPkg.depsRemoved.length;
+ var ending = "y: ";
+ var maxRevDepsDisplayed = 5;
+ var d = 0;
+ if (depsRemovedLength > 1) {
+ ending = "ies: ";
+ }
+ numPackageString = (depsRemovedLength + 1) + " packages";
+ revDepList = " and its " + depsRemovedLength + " reverse dependenc" + ending;
+ for (i in targetPkg.depsRemoved){
+ /* include up to maxRevDepsDisplayed rev deps on the page notification */
+ var notShownCount = depsRemovedLength - maxRevDepsDisplayed;
+ dep = targetPkg.depsRemoved[i];
+ if (d < maxRevDepsDisplayed) {
+ if (d > 0) {
+ revDepList += ", ";
+ }
+ revDepList += dep.name;
+ d++;
+ if ((d === maxRevDepsDisplayed) && (notShownCount > 0)) {
+ revDepList += " and " + notShownCount + " more";
+ }
+ }
+
+ /* Add any cells currently in view to the list of cells which get
+ * an inline notification inside them and which change add/rm state
+ */
+ depBtnCell = $("#package-btn-cell-" + dep.pk);
+ btnCell = btnCell.add(depBtnCell);
+
+ inlineNotify = inlineNotify.add(
+ depBtnCell.children(".inline-notification"));
+ }
+ }
+ msg+= "removed " + numPackageString + " from " + ctx.recipe.name + ":";
+ msg += " <strong>" + targetPkg.name + "</strong>";
+ msg += revDepList;
+
+ inlineNotify.text(numPackageString + " removed");
} else {
throw("Unknown package directive: should be add or remove");
}
$.ajax({
type: method,
- url: pkgBtnData.packageUrl,
+ url: targetPkg.packageUrl,
headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
success: function(data){
- /* Invalidate the Add | Rm package table's current cache */
- tableParams.nocache = true;
- $.get(ctx.tableApiUrl, tableParams);
- /* Swap the buttons around */
- pkgBtn.hide();
- $(buttonToShow).show();
+ if (data.error !== 'ok'){
+ console.warn(data.error);
+ return;
+ }
+
+ libtoaster.showChangeNotification(msg);
+
+ /* do the in-cell/inline notification to swap buttoms from add to
+ * remove
+ */
+ btnCell.children("button").fadeOut().promise().done(function(){
+ inlineNotify.fadeIn().delay(500).fadeOut(function(){
+ if (targetPkg.directive === 'add')
+ btnCell.children("button[data-directive=remove]").fadeIn();
+ else
+ btnCell.children("button[data-directive=add]").fadeIn();
+ });
+ });
+
+ /* Update the total num packages */
+ $.ajax({
+ type: "GET",
+ url: ctx.recipe.xhrPackageListUrl,
+ headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
+ success: function(data){
+ console.log(data);
+ $("#total-num-packages").text(data.total);
+ $("#total-size-packages").text(data.total_size_formatted);
+ }
+ });
}
});
}
+
+ $("#no-results-show-all-packages").click(function(){
+ $(".no-results-search-input").val("");
+ });
+
+ $("#no-results-remove-search-btn").click(function(){
+ $(".no-results-search-input").val("");
+ $(this).hide();
+ });
+
+ /* Trigger a build of your custom image */
+ $(".build-custom-image").click(function(){
+ libtoaster.startABuild(null, ctx.recipe.name,
+ function(){
+ window.location.replace(libtoaster.ctx.projectBuildsUrl);
+ });
+ });
}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/importlayer.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/importlayer.js
index c68f3669f..5a59799bc 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/importlayer.js
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/importlayer.js
@@ -18,10 +18,38 @@ function importLayerPageInit (ctx) {
libtoaster.makeTypeahead(layerDepInput, libtoaster.ctx.layersTypeAheadUrl, { include_added: "true" }, function(item){
currentLayerDepSelection = item;
+ });
+
+ // choices available in the typeahead
+ var layerDepsChoices = {};
+
+ // when the typeahead choices change, store an array of the available layer
+ // choices locally, to use for enabling/disabling the "Add layer" button
+ layerDepInput.on("typeahead-choices-change", function (event, data) {
+ layerDepsChoices = {};
- layerDepBtn.removeAttr("disabled");
+ if (data.choices) {
+ data.choices.forEach(function (item) {
+ layerDepsChoices[item.name] = item;
+ });
+ }
});
+ // disable the "Add layer" button when the layer input typeahead is empty
+ // or not in the typeahead choices
+ layerDepInput.on("input change", function () {
+ // get the choices from the typeahead
+ var choice = layerDepsChoices[$(this).val()];
+
+ if (choice) {
+ layerDepBtn.removeAttr("disabled");
+ currentLayerDepSelection = choice;
+ }
+ else {
+ layerDepBtn.attr("disabled", "disabled");
+ currentLayerDepSelection = undefined;
+ }
+ });
/* We automatically add "openembedded-core" layer for convenience as a
* dependency as pretty much all layers depend on this one
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
index 7318b3f50..259271df3 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
@@ -42,7 +42,7 @@ function layerBtnsInit() {
});
});
} else {
- notification.text("1 layer deleted");
+ notification.text("1 layer removed");
/* Deleting a layer we only hanlde the one button */
thisBtn.fadeOut(function(){
notification.fadeIn().delay(500).fadeOut(function(){
@@ -60,8 +60,7 @@ function layerBtnsInit() {
e.preventDefault();
var recipe = $(this).data('recipe-name');
- libtoaster.startABuild(libtoaster.ctx.projectBuildsUrl,
- libtoaster.ctx.projectId, recipe,
+ libtoaster.startABuild(null, recipe,
function(){
/* Success */
window.location.replace(libtoaster.ctx.projectBuildsUrl);
@@ -77,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/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerdetails.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerdetails.js
index 8c2ec4cdd..d54540626 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerdetails.js
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerdetails.js
@@ -32,21 +32,6 @@ function layerDetailsPageInit (ctx) {
}
});
-
- $(".breadcrumb li:first a").click(function(e){
- e.preventDefault();
- /* By default this link goes to the project configuration page. However
- * if we have some builds we go there instead of the default href
- */
- libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, function(prjInfo){
- if (prjInfo.builds && prjInfo.builds.length > 0) {
- window.location.replace(libtoaster.ctx.projectBuildsUrl);
- } else {
- window.location.replace(libtoaster.ctx.projectPageUrl);
- }
- });
- });
-
function addRemoveDep(depLayerId, add, doneCb) {
var data = { layer_version_id : ctx.layerVersion.id };
if (add)
@@ -258,7 +243,7 @@ function layerDetailsPageInit (ctx) {
$(".select-machine-btn").removeAttr("disabled");
addRmLayerBtn.addClass("btn-danger");
addRmLayerBtn.data('directive', "remove");
- addRmLayerBtn.text(" Delete the "+ctx.layerVersion.name+" layer from your project");
+ addRmLayerBtn.text(" Remove the "+ctx.layerVersion.name+" layer from your project");
addRmLayerBtn.prepend("<span class=\"icon-trash\"></span>");
} else {
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/libtoaster.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
index c04f7aba2..43930a2c3 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
@@ -21,6 +21,9 @@ var libtoaster = (function (){
var xhrReq;
jQElement.typeahead({
+ // each time the typeahead's choices change, a
+ // "typeahead-choices-change" event is fired with an object
+ // containing the available choices in a "choices" property
source: function(query, process){
xhrParams.search = query;
@@ -36,6 +39,8 @@ var libtoaster = (function (){
xhrReq = null;
+ jQElement.trigger("typeahead-choices-change", {choices: data.results});
+
return process(data.results);
});
},
@@ -90,27 +95,35 @@ var libtoaster = (function (){
jQElement.data('typeahead').render = customRenderFunc;
}
- /*
- * url - the url of the xhr build */
- function _startABuild (url, project_id, targets, onsuccess, onfail) {
+ /* startABuild:
+ * url: xhr_buildrequest or null for current project
+ * targets: an array or space separated list of targets to build
+ * onsuccess: callback for successful execution
+ * onfail: callback for failed execution
+ */
+ function _startABuild (url, targets, onsuccess, onfail) {
- var data = {
- project_id : project_id,
- targets : targets,
+ if (!url)
+ url = libtoaster.ctx.xhrBuildRequestUrl;
+
+ /* Flatten the array of targets into a space spearated list */
+ if (targets instanceof Array){
+ targets = targets.reduce(function(prevV, nextV){
+ return prev + ' ' + next;
+ });
}
$.ajax( {
type: "POST",
url: url,
- data: data,
+ data: { 'targets' : targets },
headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
success: function (_data) {
- /* No proper reponse YOCTO #7995
if (_data.error !== "ok") {
console.warn(_data.error);
- } else { */
+ } else {
if (onsuccess !== undefined) onsuccess(_data);
- // }
+ }
},
error: function (_data) {
console.warn("Call failed");
@@ -120,22 +133,25 @@ var libtoaster = (function (){
}
/* cancelABuild:
- * url: projectbuilds
- * builds_ids: space separated list of build request ids
+ * url: xhr_buildrequest url or null for current project
+ * buildRequestIds: space separated list of build request ids
* onsuccess: callback for successful execution
* onfail: callback for failed execution
*/
- function _cancelABuild(url, build_ids, onsuccess, onfail){
+ function _cancelABuild(url, buildRequestIds, onsuccess, onfail){
+ if (!url)
+ url = libtoaster.ctx.xhrBuildRequestUrl;
+
$.ajax( {
type: "POST",
url: url,
- data: { 'buildCancel': build_ids },
+ data: { 'buildCancel': buildRequestIds },
headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
success: function (_data) {
if (_data.error !== "ok") {
console.warn(_data.error);
} else {
- if (onsuccess !== undefined) onsuccess(_data);
+ if (onsuccess) onsuccess(_data);
}
},
error: function (_data) {
@@ -316,7 +332,7 @@ var libtoaster = (function (){
} else if (layerDepsList.length === 0 && add === true) {
alertMsg = $("<span>You have added <strong>1</strong> layer to your project: <a id=\"layer-affected-name\"></a></span></span>");
} else if (add === false) {
- alertMsg = $("<span>You have deleted <strong>1</strong> layer from your project: <a id=\"layer-affected-name\"></a></span>");
+ alertMsg = $("<span>You have removed <strong>1</strong> layer from your project: <a id=\"layer-affected-name\"></a></span>");
}
alertMsg.children("#layer-affected-name").text(layer.name);
@@ -332,6 +348,32 @@ var libtoaster = (function (){
$("#change-notification, #change-notification *").fadeIn();
}
+ function _createCustomRecipe(name, baseRecipeId, doneCb){
+ var data = {
+ 'name' : name,
+ 'project' : libtoaster.ctx.projectId,
+ 'base' : baseRecipeId,
+ };
+
+ $.ajax({
+ type: "POST",
+ url: libtoaster.ctx.xhrCustomRecipeUrl,
+ data: data,
+ headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
+ success: function (ret) {
+ if (doneCb){
+ doneCb(ret);
+ } else if (ret.error !== "ok") {
+ console.warn(ret.error);
+ }
+ },
+ error: function (ret) {
+ console.warn("Call failed");
+ console.warn(ret);
+ }
+ });
+ }
+
return {
reload_params : reload_params,
@@ -347,6 +389,7 @@ var libtoaster = (function (){
addRmLayer : _addRmLayer,
makeLayerAddRmAlertMsg : _makeLayerAddRmAlertMsg,
showChangeNotification : _showChangeNotification,
+ createCustomRecipe: _createCustomRecipe,
};
})();
@@ -443,15 +486,21 @@ $(document).ready(function() {
$('.tooltip').hide();
});
- // enable help information tooltip
- $(".get-help").tooltip({container:'body', html:true, delay:{show:300}});
+ /* Initialise bootstrap tooltips */
+ $(".get-help, [data-toggle=tooltip]").tooltip({
+ container : 'body',
+ html : true,
+ delay: { show : 300 }
+ });
// show help bubble only on hover inside tables
$(".hover-help").css("visibility","hidden");
- $("th, td").hover(function () {
+
+ $("table").on("mouseover", "th, td", function () {
$(this).find(".hover-help").css("visibility","visible");
});
- $("th, td").mouseleave(function () {
+
+ $("table").on("mouseleave", "th, td", function () {
$(this).find(".hover-help").css("visibility","hidden");
});
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/mrbsection.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
new file mode 100644
index 000000000..09117e1da
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
@@ -0,0 +1,95 @@
+
+function mrbSectionInit(ctx){
+
+ var projectBuilds;
+
+ if (ctx.mrbType === 'project')
+ projectBuilds = true;
+
+ $(".cancel-build-btn").click(function(e){
+ e.preventDefault();
+
+ var url = $(this).data('request-url');
+ var buildReqIds = $(this).data('buildrequest-id');
+ var banner = $(this).parents(".alert");
+
+ banner.find(".progress-info").fadeOut().promise().done(function(){
+ $("#cancelling-msg-" + buildReqIds).show();
+ console.log("cancel build");
+ libtoaster.cancelABuild(url, buildReqIds, function(){
+ if (projectBuilds == false){
+ /* the all builds page is not 'self updating' like thei
+ * project Builds
+ */
+ window.location.reload();
+ }
+ }, null);
+ });
+ });
+
+ $(".run-again-btn").click(function(e){
+ e.preventDefault();
+
+ var url = $(this).data('request-url');
+ var target = $(this).data('target');
+
+ libtoaster.startABuild(url, target, function(){
+ window.location.reload();
+ }, null);
+ });
+
+
+ var progressTimer;
+
+ if (projectBuilds === true){
+ progressTimer = window.setInterval(function() {
+ libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl,
+ function(prjInfo){
+ /* These two are needed because a build can be 100% and still
+ * in progress due to the fact that the % done is updated at the
+ * start of a task so it can be doing the last task at 100%
+ */
+ var inProgress = 0;
+ var allPercentDone = 0;
+ if (prjInfo.builds.length === 0)
+ return
+
+ for (var i in prjInfo.builds){
+ var build = prjInfo.builds[i];
+
+ if (build.outcome === "In Progress" ||
+ $(".progress .bar").length > 0){
+ /* Update the build progress */
+ var percentDone;
+
+ if (build.outcome !== "In Progress"){
+ /* We have to ignore the value when it's Succeeded because it
+ * goes back to 0
+ */
+ percentDone = 100;
+ } else {
+ percentDone = build.percentDone;
+ inProgress++;
+ }
+
+ $("#build-pc-done-" + build.id).text(percentDone);
+ $("#build-pc-done-title-" + build.id).attr("title", percentDone);
+ $("#build-pc-done-bar-" + build.id).css("width",
+ String(percentDone) + "%");
+
+ allPercentDone += percentDone;
+ }
+ }
+
+ if (allPercentDone === (100 * prjInfo.builds.length) && !inProgress)
+ window.location.reload();
+
+ /* Our progress bar is not still showing so shutdown the polling. */
+ if ($(".progress .bar").length === 0)
+ window.clearInterval(progressTimer);
+
+ });
+ }, 1500);
+ }
+}
+
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage.js
deleted file mode 100644
index 935b21ede..000000000
--- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage.js
+++ /dev/null
@@ -1,49 +0,0 @@
-"use strict";
-
-function newCustomImagePageInit(ctx){
-
- var newCustomImgBtn = $("#create-new-custom-image-btn");
- var imgCustomModal = $("#new-custom-image-modal");
-
- newCustomImgBtn.click(function(e){
- e.preventDefault();
-
- var name = imgCustomModal.find('input').val();
- var baseRecipeId = imgCustomModal.data('recipe');
-
- if (name.length > 0) {
- createCustomRecipe(name, baseRecipeId);
- imgCustomModal.modal('hide');
- } else {
- console.warn("TODO No name supplied");
- }
- });
-
- function createCustomRecipe(name, baseRecipeId){
- var data = {
- 'name' : name,
- 'project' : libtoaster.ctx.projectId,
- 'base' : baseRecipeId,
- };
-
- $.ajax({
- type: "POST",
- url: ctx.xhrCustomRecipeUrl,
- data: data,
- headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
- success: function (ret) {
- if (ret.error !== "ok") {
- console.warn(ret.error);
- } else {
- window.location.replace(ret.url + '?notify=new');
- }
- },
- error: function (ret) {
- console.warn("Call failed");
- console.warn(ret);
- }
- });
- }
-
-
-}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
new file mode 100644
index 000000000..cb9ed4da0
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage_modal.js
@@ -0,0 +1,148 @@
+"use strict";
+
+/*
+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 invalidNameMsg = "Image names cannot contain spaces or capital letters. The only allowed special character is dash (-).";
+ 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,
+ // 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") {
+ showNameError(invalidNameMsg);
+ return;
+ } else if (ret.error === "recipe-already-exists") {
+ showNameError(duplicateNameMsg);
+ return;
+ } else if (ret.error === "image-already-exists") {
+ showNameError(duplicateImageInProjectMsg);
+ return;
+ }
+ } else {
+ imgCustomModal.modal('hide');
+ window.location.replace(ret.url + '?notify=new');
+ }
+ });
+ }
+ });
+
+ 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);
+ return
+ }
+
+ if (nameInput.val().search(/[^a-z|0-9|-]/) != -1){
+ showNameError(invalidNameMsg);
+ newCustomImgBtn.prop("disabled", true);
+ nameInput.parent().addClass('error');
+ } else {
+ invalidNameHelp.hide();
+ newCustomImgBtn.prop("disabled", false);
+ nameInput.parent().removeClass('error');
+ }
+ });
+}
+
+// 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': <recipe ID>, 'name': <recipe 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"]');
+
+ // remove any existing radio buttons + labels
+ imageSelector.remove('[data-role="image-radio"]');
+
+ if (baseRecipes.length === 1) {
+ // hide the radio button container
+ imageSelector.hide();
+
+ // 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(
+ '<label class="radio" data-role="image-radio">' +
+ recipe.name +
+ '<input type="radio" class="form-control" name="select-image" ' +
+ 'data-recipe="' + recipe.id + '">' +
+ '</label>'
+ );
+ }
+
+ // show the radio button container
+ imageSelector.show();
+ }
+}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projectpage.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projectpage.js
index e742ef291..3013416dd 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projectpage.js
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projectpage.js
@@ -116,6 +116,8 @@ function projectPageInit(ctx) {
addRmLayer(layerObj, true);
/* Reset the text input */
layerAddInput.val("");
+ /* Disable the add layer button*/
+ layerAddBtn.attr("disabled", "disabled");
});
function addRmLayer(layerObj, add){
@@ -143,7 +145,7 @@ function projectPageInit(ctx) {
for (var i in layers){
var layerObj = layers[i];
- var projectLayer = $("<li><a></a><span class=\"icon-trash\" data-toggle=\"tooltip\" title=\"Delete\"></span></li>");
+ var projectLayer = $("<li><a></a><span class=\"icon-trash\" data-toggle=\"tooltip\" title=\"Remove\"></span></li>");
projectLayer.data('layer', layerObj);
projectLayer.children("span").tooltip();
@@ -175,11 +177,18 @@ function projectPageInit(ctx) {
function updateLayersCount(){
var count = $("#layers-in-project-list").children().length;
+ var noLayerMsg = $("#no-layers-in-project");
+ var buildInput = $("#build-input");
- if (count === 0)
+
+ if (count === 0) {
+ noLayerMsg.fadeIn();
$("#no-layers-in-project").fadeIn();
- else
- $("#no-layers-in-project").hide();
+ buildInput.attr("disabled", "disabled");
+ } else {
+ noLayerMsg.hide();
+ buildInput.removeAttr("disabled");
+ }
$("#project-layers-count").text(count);
@@ -218,17 +227,19 @@ function projectPageInit(ctx) {
var toBuild = "";
freqBuildList.find(":checked").each(function(){
- toBuild += $(this).val();
+ toBuild += $(this).val() + ' ';
});
- libtoaster.startABuild(libtoaster.ctx.projectBuildsUrl, libtoaster.ctx.projectId, toBuild, function(){
- /* Build started */
- window.location.replace(libtoaster.ctx.projectBuildsUrl);
- },
- function(){
- /* Build start failed */
- /* [YOCTO #7995] */
- window.location.replace(libtoaster.ctx.projectBuildsUrl);
+ toBuild = toBuild.trim();
+
+ libtoaster.startABuild(null, toBuild,
+ function(){
+ /* Build request started */
+ window.location.replace(libtoaster.ctx.projectBuildsUrl);
+ },
+ function(){
+ /* Build request failed */
+ console.warn("Build request failed to be created");
});
});
@@ -380,7 +391,7 @@ function projectPageInit(ctx) {
/* Layers removed */
if (layersToRm && layersToRm.length > 0){
if (layersToRm.length == 1)
- li = '<li><strong>1</strong> layer deleted: '+layersToRm[0].name+'</li>';
+ li = '<li><strong>1</strong> layer removed: '+layersToRm[0].name+'</li>';
else
li = '<li><strong>'+layersToRm.length+'</strong> layers deleted: '+layersDelList+'</li>';
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
new file mode 100644
index 000000000..b09f974e4
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
@@ -0,0 +1,90 @@
+'use strict';
+
+function projectTopBarInit(ctx) {
+
+ var projectNameForm = $("#project-name-change-form");
+ var projectNameContainer = $("#project-name-container");
+ var projectName = $("#project-name");
+ var projectNameFormToggle = $("#project-change-form-toggle");
+ var projectNameChangeCancel = $("#project-name-change-cancel");
+
+ // this doesn't exist for command-line builds
+ var newBuildTargetInput = $("#build-input");
+
+ var newBuildTargetBuildBtn = $("#build-button");
+ var selectedTarget;
+
+ /* Project name change functionality */
+ projectNameFormToggle.click(function(e){
+ e.preventDefault();
+ projectNameContainer.hide();
+ projectNameForm.fadeIn();
+ });
+
+ projectNameChangeCancel.click(function(e){
+ e.preventDefault();
+ projectNameForm.hide();
+ projectNameContainer.fadeIn();
+ });
+
+ $("#project-name-change-btn").click(function(){
+ var newProjectName = $("#project-name-change-input").val();
+
+ libtoaster.editCurrentProject({ projectName: newProjectName }, function (){
+ projectName.html(newProjectName);
+ libtoaster.ctx.projectName = newProjectName;
+ projectNameChangeCancel.click();
+ });
+ });
+
+ /* Nav bar activate state switcher */
+ $("#project-topbar .nav li a").each(function(){
+ if (window.location.pathname === $(this).attr('href'))
+ $(this).parent().addClass('active');
+ else
+ $(this).parent().removeClass('active');
+ });
+
+ if (!newBuildTargetInput.length) {
+ return;
+ }
+
+ /* the following only applies for non-command-line projects */
+
+ /* Recipe build input functionality */
+ if (ctx.numProjectLayers > 0 && ctx.machine){
+ newBuildTargetInput.removeAttr("disabled");
+ }
+
+ libtoaster.makeTypeahead(newBuildTargetInput,
+ libtoaster.ctx.recipesTypeAheadUrl, {}, function (item) {
+ selectedTarget = item;
+ newBuildTargetBuildBtn.removeAttr("disabled");
+ });
+
+ newBuildTargetInput.on('input', function () {
+ if ($(this).val().length === 0) {
+ newBuildTargetBuildBtn.attr("disabled", "disabled");
+ } else {
+ newBuildTargetBuildBtn.removeAttr("disabled");
+ }
+ });
+
+ newBuildTargetBuildBtn.click(function (e) {
+ e.preventDefault();
+ if (!newBuildTargetInput.val()) {
+ return;
+ }
+ /* We use the value of the input field so as to maintain any command also
+ * added e.g. core-image-minimal:clean and because we can build targets
+ * that toaster doesn't yet know about
+ */
+ selectedTarget = { name: newBuildTargetInput.val() };
+
+ /* Fire off the build */
+ libtoaster.startABuild(null, selectedTarget.name,
+ function(){
+ window.location.replace(libtoaster.ctx.projectBuildsUrl);
+ }, null);
+ });
+}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/qunit-1.18.0.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/qunit-1.18.0.js
new file mode 100644
index 000000000..fadd3403a
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/qunit-1.18.0.js
@@ -0,0 +1,347 @@
+(function(window){var QUnit,config,onErrorFnPrev,loggingCallbacks={},fileName=(sourceFromStacktrace(0)||"").replace(/(:\d+)+\)?/,"").replace(/.+\//,""),toString=Object.prototype.toString,hasOwn=Object.prototype.hasOwnProperty,Date=window.Date,now=Date.now||function(){return new Date().getTime();},globalStartCalled=false,runStarted=false,setTimeout=window.setTimeout,clearTimeout=window.clearTimeout,defined={document:window.document!==undefined,setTimeout:window.setTimeout!==undefined,sessionStorage:(function(){var x="qunit-test-string";try{sessionStorage.setItem(x,x);sessionStorage.removeItem(x);return true;}catch(e){return false;}}())},errorString=function(error){var name,message,errorString=error.toString();if(errorString.substring(0,7)==="[object"){name=error.name?error.name.toString():"Error";message=error.message?error.message.toString():"";if(name&&message){return name+": "+message;}else if(name){return name;}else if(message){return message;}else{return "Error";}}else{return errorString;}},objectValues =function(obj){var key,val,vals=QUnit.is("array",obj)?[]:{};for(key in obj){if(hasOwn.call(obj,key)){val=obj[key];vals[key]=val===Object(val)?objectValues(val):val;}}
+return vals;};QUnit={};config={queue:[],blocking:true,reorder:true,altertitle:true,scrolltop:true,requireExpects:false,maxDepth:5,urlConfig:[{id:"hidepassed",label:"Hide passed tests",tooltip:"Only show tests and assertions that fail. Stored as query-strings."},{id:"noglobals",label:"Check for Globals",tooltip:"Enabling this will test if any test introduces new properties on the "+
+"`window` object. Stored as query-strings."},{id:"notrycatch",label:"No try-catch",tooltip:"Enabling this will run tests outside of a try-catch block. Makes debugging "+
+"exceptions in IE reasonable. Stored as query-strings."}],modules:[],currentModule:{name:"",tests:[]},callbacks:{}};config.modules.push(config.currentModule);(function(){var i,current,location=window.location||{search:"",protocol:"file:"},params=location.search.slice(1).split("&"),length=params.length,urlParams={};if(params[0]){for(i=0;i<length;i++){current=params[i].split("=");current[0]=decodeURIComponent(current[0]);current[1]=current[1]?decodeURIComponent(current[1]):true;if(urlParams[current[0]]){urlParams[current[0]]=[].concat(urlParams[current[0]],current[1]);}else{urlParams[current[0]]=current[1];}}}
+if(urlParams.filter===true){delete urlParams.filter;}
+QUnit.urlParams=urlParams;config.filter=urlParams.filter;if(urlParams.maxDepth){config.maxDepth=parseInt(urlParams.maxDepth,10)===-1?Number.POSITIVE_INFINITY:urlParams.maxDepth;}
+config.testId=[];if(urlParams.testId){urlParams.testId=decodeURIComponent(urlParams.testId).split(",");for(i=0;i<urlParams.testId.length;i++){config.testId.push(urlParams.testId[i]);}}
+QUnit.isLocal=location.protocol==="file:";QUnit.version="1.18.0";}());extend(QUnit,{module:function(name,testEnvironment){var currentModule={name:name,testEnvironment:testEnvironment,tests:[]};if(testEnvironment&&testEnvironment.setup){testEnvironment.beforeEach=testEnvironment.setup;delete testEnvironment.setup;}
+if(testEnvironment&&testEnvironment.teardown){testEnvironment.afterEach=testEnvironment.teardown;delete testEnvironment.teardown;}
+config.modules.push(currentModule);config.currentModule=currentModule;},asyncTest:function(testName,expected,callback){if(arguments.length===2){callback=expected;expected=null;}
+QUnit.test(testName,expected,callback,true);},test:function(testName,expected,callback,async){var test;if(arguments.length===2){callback=expected;expected=null;}
+test=new Test({testName:testName,expected:expected,async:async,callback:callback});test.queue();},skip:function(testName){var test=new Test({testName:testName,skip:true});test.queue();},//
+start:function(count){var globalStartAlreadyCalled=globalStartCalled;if(!config.current){globalStartCalled=true;if(runStarted){throw new Error("Called start() outside of a test context while already started");}else if(globalStartAlreadyCalled||count>1){throw new Error("Called start() outside of a test context too many times");}else if(config.autostart){throw new Error("Called start() outside of a test context when "+
+"QUnit.config.autostart was true");}else if(!config.pageLoaded){config.autostart=true;return;}}else{config.current.semaphore-=count||1;if(config.current.semaphore>0){return;}
+if(config.current.semaphore<0){config.current.semaphore=0;QUnit.pushFailure("Called start() while already started (test's semaphore was 0 already)",sourceFromStacktrace(2));return;}}
+resumeProcessing();},stop:function(count){if(!config.current){throw new Error("Called stop() outside of a test context");}
+config.current.semaphore+=count||1;pauseProcessing();},config:config,is:function(type,obj){return QUnit.objectType(obj)===type;},objectType:function(obj){if(typeof obj==="undefined"){return "undefined";}//
+if(obj===null){return "null";}
+var match=toString.call(obj).match(/^\[object\s(.*)\]$/),type=match&&match[1]||"";switch(type){case "Number":if(isNaN(obj)){return "nan";}
+return "number";case "String":case "Boolean":case "Array":case "Date":case "RegExp":case "Function":return type.toLowerCase();}
+if(typeof obj==="object"){return "object";}
+return undefined;},extend:extend,load:function(){config.pageLoaded=true;extend(config,{stats:{all:0,bad:0},moduleStats:{all:0,bad:0},started:0,updateRate:1000,autostart:true,filter:""},true);config.blocking=false;if(config.autostart){resumeProcessing();}}});(function(){var i,l,key,callbacks=["begin","done","log","testStart","testDone","moduleStart","moduleDone"];function registerLoggingCallback(key){var loggingCallback=function(callback){if(QUnit.objectType(callback)!=="function"){throw new Error("QUnit logging methods require a callback function as their first parameters.");}
+config.callbacks[key].push(callback);};loggingCallbacks[key]=loggingCallback;return loggingCallback;}
+for(i=0,l=callbacks.length;i<l;i++){key=callbacks[i];if(QUnit.objectType(config.callbacks[key])==="undefined"){config.callbacks[key]=[];}
+QUnit[key]=registerLoggingCallback(key);}})();onErrorFnPrev=window.onerror;//
+window.onerror=function(error,filePath,linerNr){var ret=false;if(onErrorFnPrev){ret=onErrorFnPrev(error,filePath,linerNr);}
+if(ret!==true){if(QUnit.config.current){if(QUnit.config.current.ignoreGlobalErrors){return true;}
+QUnit.pushFailure(error,filePath+":"+linerNr);}else{QUnit.test("global failure",extend(function(){QUnit.pushFailure(error,filePath+":"+linerNr);},{validTest:true}));}
+return false;}
+return ret;};function done(){var runtime,passed;config.autorun=true;if(config.previousModule){runLoggingCallbacks("moduleDone",{name:config.previousModule.name,tests:config.previousModule.tests,failed:config.moduleStats.bad,passed:config.moduleStats.all-config.moduleStats.bad,total:config.moduleStats.all,runtime:now()-config.moduleStats.started});}
+delete config.previousModule;runtime=now()-config.started;passed=config.stats.all-config.stats.bad;runLoggingCallbacks("done",{failed:config.stats.bad,passed:passed,total:config.stats.all,runtime:runtime});}
+function extractStacktrace(e,offset){offset=offset===undefined?4:offset;var stack,include,i;if(e.stack){stack=e.stack.split("\n");if(/^error$/i.test(stack[0])){stack.shift();}
+if(fileName){include=[];for(i=offset;i<stack.length;i++){if(stack[i].indexOf(fileName)!==-1){break;}
+include.push(stack[i]);}
+if(include.length){return include.join("\n");}}
+return stack[offset];//
+}else if(e.sourceURL){if(/qunit.js$/.test(e.sourceURL)){return;}
+return e.sourceURL+":"+e.line;}}
+function sourceFromStacktrace(offset){var error=new Error();if(!error.stack){try{throw error;}catch(err){error=err;}}
+return extractStacktrace(error,offset);}
+function synchronize(callback,last){if(QUnit.objectType(callback)==="array"){while(callback.length){synchronize(callback.shift());}
+return;}
+config.queue.push(callback);if(config.autorun&&!config.blocking){process(last);}}
+function process(last){function next(){process(last);}
+var start=now();config.depth=(config.depth||0)+1;while(config.queue.length&&!config.blocking){if(!defined.setTimeout||config.updateRate<=0||((now()-start)<config.updateRate)){if(config.current){config.current.usedAsync=false;}
+config.queue.shift()();}else{setTimeout(next,13);break;}}
+config.depth--;if(last&&!config.blocking&&!config.queue.length&&config.depth===0){done();}}
+function begin(){var i,l,modulesLog=[];if(!config.started){config.started=now();verifyLoggingCallbacks();if(config.modules[0].name===""&&config.modules[0].tests.length===0){config.modules.shift();}
+for(i=0,l=config.modules.length;i<l;i++){modulesLog.push({name:config.modules[i].name,tests:config.modules[i].tests});}
+runLoggingCallbacks("begin",{totalTests:Test.count,modules:modulesLog});}
+config.blocking=false;process(true);}
+function resumeProcessing(){runStarted=true;if(defined.setTimeout){setTimeout(function(){if(config.current&&config.current.semaphore>0){return;}
+if(config.timeout){clearTimeout(config.timeout);}
+begin();},13);}else{begin();}}
+function pauseProcessing(){config.blocking=true;if(config.testTimeout&&defined.setTimeout){clearTimeout(config.timeout);config.timeout=setTimeout(function(){if(config.current){config.current.semaphore=0;QUnit.pushFailure("Test timed out",sourceFromStacktrace(2));}else{throw new Error("Test timed out");}
+resumeProcessing();},config.testTimeout);}}
+function saveGlobal(){config.pollution=[];if (config.noglobals){for(var key in window){if(hasOwn.call(window,key)){if(/^qunit-test-output/.test(key)){continue;}
+config.pollution.push(key);}}}}
+function checkPollution(){var newGlobals,deletedGlobals,old=config.pollution;saveGlobal();newGlobals=diff(config.pollution,old);if(newGlobals.length>0){QUnit.pushFailure("Introduced global variable(s): "+newGlobals.join(", "));}
+deletedGlobals=diff(old,config.pollution);if(deletedGlobals.length>0){QUnit.pushFailure("Deleted global variable(s): "+deletedGlobals.join(", "));}}
+function diff(a,b){var i,j,result=a.slice();for(i=0;i<result.length;i++){for(j=0;j<b.length;j++){if(result[i]===b[j]){result.splice(i,1);i--;break;}}}
+return result;}
+function extend(a,b,undefOnly){for(var prop in b){if(hasOwn.call(b,prop)){if(!(prop==="constructor"&&a===window)){if(b[prop]===undefined){delete a[prop];}else if(!(undefOnly&&typeof a[prop]!=="undefined")){a[prop]=b[prop];}}}}
+return a;}
+function runLoggingCallbacks(key,args){var i,l,callbacks;callbacks=config.callbacks[key];for(i=0,l=callbacks.length;i<l;i++){callbacks[i](args);}}
+function verifyLoggingCallbacks(){var loggingCallback,userCallback;for(loggingCallback in loggingCallbacks){if(QUnit[loggingCallback]!==loggingCallbacks[loggingCallback]){userCallback=QUnit[loggingCallback];QUnit[loggingCallback]=loggingCallbacks[loggingCallback];QUnit[loggingCallback](userCallback);if(window.console&&window.console.warn){window.console.warn("QUnit."+loggingCallback+" was replaced with a new value.\n"+
+"Please, check out the documentation on how to apply logging callbacks.\n"+
+"Reference: http://api.qunitjs.com/category/callbacks/");}}}}
+function inArray(elem,array){if(array.indexOf){return array.indexOf(elem);}
+for(var i=0,length=array.length;i<length;i++){if(array[i]===elem){return i;}}
+return-1;}
+function Test(settings){var i,l;++Test.count;extend(this,settings);this.assertions=[];this.semaphore=0;this.usedAsync=false;this.module=config.currentModule;this.stack=sourceFromStacktrace(3);for(i=0,l=this.module.tests;i<l.length;i++){if(this.module.tests[i].name===this.testName){this.testName+=" ";}}
+this.testId=generateHash(this.module.name,this.testName);this.module.tests.push({name:this.testName,testId:this.testId});if(settings.skip){this.callback=function(){};this.async=false;this.expected=0;}else{this.assert=new Assert(this);}}
+Test.count=0;Test.prototype={before:function(){if(this.module!==config.previousModule||//
+!hasOwn.call(config,"previousModule")){if(hasOwn.call(config,"previousModule")){runLoggingCallbacks("moduleDone",{name:config.previousModule.name,tests:config.previousModule.tests,failed:config.moduleStats.bad,passed:config.moduleStats.all-config.moduleStats.bad,total:config.moduleStats.all,runtime:now()-config.moduleStats.started});}
+config.previousModule=this.module;config.moduleStats={all:0,bad:0,started:now()};runLoggingCallbacks("moduleStart",{name:this.module.name,tests:this.module.tests});}
+config.current=this;this.testEnvironment=extend({},this.module.testEnvironment);delete this.testEnvironment.beforeEach;delete this.testEnvironment.afterEach;this.started=now();runLoggingCallbacks("testStart",{name:this.testName,module:this.module.name,testId:this.testId});if(!config.pollution){saveGlobal();}},run:function(){var promise;config.current=this;if(this.async){QUnit.stop();}
+this.callbackStarted=now();if(config.notrycatch){promise=this.callback.call(this.testEnvironment,this.assert);this.resolvePromise(promise);return;}
+try{promise=this.callback.call(this.testEnvironment,this.assert);this.resolvePromise(promise);}catch(e){this.pushFailure("Died on test #"+(this.assertions.length+1)+" "+
+this.stack+": "+(e.message||e),extractStacktrace(e,0));saveGlobal();if(config.blocking){QUnit.start();}}},after:function(){checkPollution();},queueHook:function(hook,hookName){var promise,test=this;return function runHook(){config.current=test;if(config.notrycatch){promise=hook.call(test.testEnvironment,test.assert);test.resolvePromise(promise,hookName);return;}
+try{promise=hook.call(test.testEnvironment,test.assert);test.resolvePromise(promise,hookName);}catch(error){test.pushFailure(hookName+" failed on "+test.testName+": "+(error.message||error),extractStacktrace(error,0));}};},hooks:function(handler){var hooks=[];if(this.skip){return hooks;}
+if(this.module.testEnvironment&&QUnit.objectType(this.module.testEnvironment[handler])==="function"){hooks.push(this.queueHook(this.module.testEnvironment[handler],handler));}
+return hooks;},finish:function(){config.current=this;if(config.requireExpects&&this.expected===null){this.pushFailure("Expected number of assertions to be defined, but expect() was "+
+"not called.",this.stack);}else if(this.expected!==null&&this.expected!==this.assertions.length){this.pushFailure("Expected "+this.expected+" assertions, but "+
+this.assertions.length+" were run",this.stack);}else if(this.expected===null&&!this.assertions.length){this.pushFailure("Expected at least one assertion, but none were run - call "+
+"expect(0) to accept zero assertions.",this.stack);}
+var i,bad=0;this.runtime=now()-this.started;config.stats.all+=this.assertions.length;config.moduleStats.all+=this.assertions.length;for(i=0;i<this.assertions.length;i++){if(!this.assertions[i].result){bad++;config.stats.bad++;config.moduleStats.bad++;}}
+runLoggingCallbacks("testDone",{name:this.testName,module:this.module.name,skipped:!!this.skip,failed:bad,passed:this.assertions.length-bad,total:this.assertions.length,runtime:this.runtime,assertions:this.assertions,testId:this.testId,duration:this.runtime});QUnit.reset();config.current=undefined;},queue:function(){var bad,test=this;if(!this.valid()){return;}
+function run(){synchronize([function(){test.before();},test.hooks("beforeEach"),function(){test.run();},test.hooks("afterEach").reverse(),function(){test.after();},function(){test.finish();}]);}
+bad=QUnit.config.reorder&&defined.sessionStorage&&+sessionStorage.getItem("qunit-test-"+this.module.name+"-"+this.testName);if(bad){run();}else{synchronize(run,true);}},push:function(result,actual,expected,message){var source,details={module:this.module.name,name:this.testName,result:result,message:message,actual:actual,expected:expected,testId:this.testId,runtime:now()-this.started};if(!result){source=sourceFromStacktrace();if(source){details.source=source;}}
+runLoggingCallbacks("log",details);this.assertions.push({result:!!result,message:message});},pushFailure:function(message,source,actual){if(!this instanceof Test){throw new Error("pushFailure() assertion outside test context, was "+
+sourceFromStacktrace(2));}
+var details={module:this.module.name,name:this.testName,result:false,message:message||"error",actual:actual||null,testId:this.testId,runtime:now()-this.started};if(source){details.source=source;}
+runLoggingCallbacks("log",details);this.assertions.push({result:false,message:message});},resolvePromise:function(promise,phase){var then,message,test=this;if(promise!=null){then=promise.then;if(QUnit.objectType(then)==="function"){QUnit.stop();then.call(promise,QUnit.start,function(error){message="Promise rejected "+(!phase?"during":phase.replace(/Each$/,""))+
+" "+test.testName+": "+(error.message||error);test.pushFailure(message,extractStacktrace(error,0));saveGlobal();QUnit.start();});}}},valid:function(){var include,filter=config.filter&&config.filter.toLowerCase(),module=QUnit.urlParams.module&&QUnit.urlParams.module.toLowerCase(),fullName=(this.module.name+": "+this.testName).toLowerCase();if(this.callback&&this.callback.validTest){return true;}
+if(config.testId.length>0&&inArray(this.testId,config.testId)<0){return false;}
+if(module&&(!this.module.name||this.module.name.toLowerCase()!==module)){return false;}
+if(!filter){return true;}
+include=filter.charAt(0)!=="!";if(!include){filter=filter.slice(1);}
+if(fullName.indexOf(filter)!==-1){return include;}
+return!include;}};QUnit.reset=function(){if(typeof window==="undefined"){return;}
+var fixture=defined.document&&document.getElementById&&document.getElementById("qunit-fixture");if(fixture){fixture.innerHTML=config.fixture;}};QUnit.pushFailure=function(){if(!QUnit.config.current){throw new Error("pushFailure() assertion outside test context, in "+
+sourceFromStacktrace(2));}
+var currentTest=QUnit.config.current;return currentTest.pushFailure.apply(currentTest,arguments);};function generateHash(module,testName){var hex,i=0,hash=0,str=module+"\x1C"+testName,len=str.length;for(;i<len;i++){hash=((hash<<5)-hash)+str.charCodeAt(i);hash|=0;}
+hex=(0x100000000+hash).toString(16);if(hex.length<8){hex="0000000"+hex;}
+return hex.slice(-8);}
+function Assert(testContext){this.test=testContext;}
+QUnit.assert=Assert.prototype={expect:function(asserts){if(arguments.length===1){this.test.expected=asserts;}else{return this.test.expected;}},async:function(){var test=this.test,popped=false;test.semaphore+=1;test.usedAsync=true;pauseProcessing();return function done(){if(!popped){test.semaphore-=1;popped=true;resumeProcessing();}else{test.pushFailure("Called the callback returned from `assert.async` more than once",sourceFromStacktrace(2));}};},push:function(){var assert=this,currentTest=(assert instanceof Assert&&assert.test)||QUnit.config.current;if(!currentTest){throw new Error("assertion outside test context, in "+sourceFromStacktrace(2));}
+if(currentTest.usedAsync===true&&currentTest.semaphore===0){currentTest.pushFailure("Assertion after the final `assert.async` was resolved",sourceFromStacktrace(2));}
+if(!(assert instanceof Assert)){assert=currentTest.assert;}
+return assert.test.push.apply(assert.test,arguments);},ok:function(result,message){message=message||(result?"okay":"failed, expected argument to be truthy, was: "+
+QUnit.dump.parse(result));this.push(!!result,result,true,message);},notOk:function(result,message){message=message||(!result?"okay":"failed, expected argument to be falsy, was: "+
+QUnit.dump.parse(result));this.push(!result,result,false,message);},equal:function(actual,expected,message){this.push(expected==actual,actual,expected,message);},notEqual:function(actual,expected,message){this.push(expected!=actual,actual,expected,message);},propEqual:function(actual,expected,message){actual=objectValues(actual);expected=objectValues(expected);this.push(QUnit.equiv(actual,expected),actual,expected,message);},notPropEqual:function(actual,expected,message){actual=objectValues(actual);expected=objectValues(expected);this.push(!QUnit.equiv(actual,expected),actual,expected,message);},deepEqual:function(actual,expected,message){this.push(QUnit.equiv(actual,expected),actual,expected,message);},notDeepEqual:function(actual,expected,message){this.push(!QUnit.equiv(actual,expected),actual,expected,message);},strictEqual:function(actual,expected,message){this.push(expected===actual,actual,expected,message);},notStrictEqual:function(actual,expected,message){this.push(expected!==actual,actual,expected,message);},"throws":function(block,expected,message){var actual,expectedType,expectedOutput=expected,ok=false,currentTest=(this instanceof Assert&&this.test)||QUnit.config.current;if(message==null&&typeof expected==="string"){message=expected;expected=null;}
+currentTest.ignoreGlobalErrors=true;try{block.call(currentTest.testEnvironment);}catch(e){actual=e;}
+currentTest.ignoreGlobalErrors=false;if(actual){expectedType=QUnit.objectType(expected);if(!expected){ok=true;expectedOutput=null;}else if(expectedType==="regexp"){ok=expected.test(errorString(actual));}else if(expectedType==="string"){ok=expected===errorString(actual);}else if(expectedType==="function"&&actual instanceof expected){ok=true;}else if(expectedType==="object"){ok=actual instanceof expected.constructor&&actual.name===expected.name&&actual.message===expected.message;}else if(expectedType==="function"&&expected.call({},actual)===true){expectedOutput= null;ok=true;}}
+currentTest.assert.push(ok,actual,expectedOutput,message);}};(function(){Assert.prototype.raises=Assert.prototype["throws"];}());QUnit.equiv=(function(){function bindCallbacks(o,callbacks,args){var prop=QUnit.objectType(o);if(prop){if(QUnit.objectType(callbacks[prop])==="function"){return callbacks[prop].apply(callbacks,args);}else{return callbacks[prop];}}}
+var innerEquiv,callers=[],parents=[],parentsB=[],getProto=Object.getPrototypeOf||function(obj){return obj.__proto__;},callbacks=(function(){function useStrictEquality(b,a){if(b instanceof a.constructor||a instanceof b.constructor){return a==b;}else{return a===b;}}
+return{"string":useStrictEquality,"boolean":useStrictEquality,"number":useStrictEquality,"null":useStrictEquality,"undefined":useStrictEquality,"nan":function(b){return isNaN(b);},"date":function(b,a){return QUnit.objectType(b)==="date"&&a.valueOf()===b.valueOf();},"regexp":function(b,a){return QUnit.objectType(b)==="regexp"&&a.source===b.source&&a.global===b.global&&a.ignoreCase===b.ignoreCase&&a.multiline===b.multiline&&a.sticky===b.sticky;},"function":function(){var caller=callers[callers.length-1];return caller!==Object&&typeof caller!=="undefined";},"array":function(b,a){var i,j,len,loop,aCircular,bCircular;if(QUnit.objectType(b)!=="array"){return false;}
+len=a.length;if(len!==b.length){return false;}
+parents.push(a);parentsB.push(b);for(i=0;i<len;i++){loop=false;for(j=0;j<parents.length;j++){aCircular=parents[j]===a[i];bCircular=parentsB[j]===b[i];if(aCircular||bCircular){ if(a[i]===b[i]||aCircular&&bCircular){loop=true;}else{parents.pop();parentsB.pop();return false;}}}
+if(!loop&&!innerEquiv(a[i],b[i])){parents.pop();parentsB.pop();return false;}}
+parents.pop();parentsB.pop();return true;},"object":function(b,a){var i,j,loop,aCircular,bCircular,eq=true,aProperties=[],bProperties=[];if(a.constructor!==b.constructor){if(!((getProto(a)===null&&getProto(b)===Object.prototype)||(getProto(b)===null&&getProto(a)===Object.prototype))){return false;}}
+callers.push(a.constructor);parents.push(a);parentsB.push(b);for(i in a){loop=false;for(j=0;j<parents.length;j++){aCircular=parents[j]===a[i];bCircular=parentsB[j]===b[i];if(aCircular||bCircular){if(a[i]===b[i]||aCircular&&bCircular){loop=true;}else{eq=false;break;}}}
+aProperties.push(i);if(!loop&&!innerEquiv(a[i],b[i])){eq=false;break;}}
+parents.pop();parentsB.pop();callers.pop();for(i in b){bProperties.push(i);}
+return eq&&innerEquiv(aProperties.sort(),bProperties.sort());}};}());innerEquiv=function(){var args=[].slice.apply(arguments);if(args.length<2){return true;}
+return((function(a,b){if(a===b){return true;}else if(a===null||b===null||typeof a==="undefined"||typeof b==="undefined"||QUnit.objectType(a)!==QUnit.objectType(b)){return false;}else{return bindCallbacks(a,callbacks,[b,a]);}}(args[0],args[1]))&&innerEquiv.apply(this,args.splice(1,args.length-1)));};return innerEquiv;}());QUnit.dump=(function(){function quote(str){return "\""+str.toString().replace(/"/g,"\\\"")+"\"";}
+function literal(o){return o+"";}
+function join(pre,arr,post){var s=dump.separator(),base=dump.indent(),inner=dump.indent(1);if(arr.join){arr=arr.join(","+s+inner);}
+if(!arr){return pre+post;}
+return[pre,inner+arr,base+post].join(s);}
+function array(arr,stack){var i=arr.length,ret=new Array(i);if(dump.maxDepth&&dump.depth>dump.maxDepth){return "[object Array]";}
+this.up();while(i--){ret[i]=this.parse(arr[i],undefined,stack);}
+this.down();return join("[",ret,"]");}
+var reName=/^function (\w+)/,dump={parse:function(obj,objType,stack){stack=stack||[];var res,parser,parserType,inStack=inArray(obj,stack);if(inStack!==-1){return "recursion("+(inStack-stack.length)+")";}
+objType=objType||this.typeOf(obj);parser=this.parsers[objType];parserType=typeof parser;if(parserType==="function"){stack.push(obj);res=parser.call(this,obj,stack);stack.pop();return res;}
+return(parserType==="string")?parser:this.parsers.error;},typeOf:function(obj){var type;if(obj===null){type="null";}else if(typeof obj==="undefined"){type="undefined";}else if(QUnit.is("regexp",obj)){type="regexp";}else if(QUnit.is("date",obj)){type="date";}else if(QUnit.is("function",obj)){type="function";}else if(obj.setInterval!==undefined&&obj.document!==undefined&&obj.nodeType===undefined){type="window";}else if(obj.nodeType===9){type="document";}else if(obj.nodeType){type="node";}else if(toString.call(obj)==="[object Array]"||(typeof obj.length==="number"&&obj.item!==undefined&&(obj.length?obj.item(0)===obj[ 0]:(obj.item(0)===null&&obj[0]===undefined)))){type="array";}else if(obj.constructor===Error.prototype.constructor){type="error";}else{type=typeof obj;}
+return type;},separator:function(){return this.multiline?this.HTML?"<br />":"\n":this.HTML?"&#160;":" ";},indent:function(extra){if(!this.multiline){return "";}
+var chr=this.indentChar;if(this.HTML){chr=chr.replace(/\t/g," ").replace(/ /g,"&#160;");}
+return new Array(this.depth+(extra||0)).join(chr);},up:function(a){this.depth+=a||1;},down:function(a){this.depth-=a||1;},setParser:function(name,parser){this.parsers[name]=parser;},quote:quote,literal:literal,join:join,depth:1,maxDepth:QUnit.config.maxDepth,parsers:{window:"[Window]",document:"[Document]",error:function(error){return "Error(\""+error.message+"\")";},unknown:"[Unknown]","null":"null","undefined":"undefined","function":function(fn){var ret="function",name="name" in fn?fn.name:(reName.exec(fn)||[])[1];if(name){ret+=" "+name;}
+ret+="( ";ret=[ret,dump.parse(fn,"functionArgs"),"){"].join("");return join(ret,dump.parse(fn,"functionCode"),"}");},array:array,nodelist:array,"arguments":array,object:function(map,stack){var keys,key,val,i,nonEnumerableProperties,ret=[];if(dump.maxDepth&&dump.depth>dump.maxDepth){return "[object Object]";}
+dump.up(); keys=[];for(key in map){keys.push(key);}
+nonEnumerableProperties=["message","name"];for(i in nonEnumerableProperties){key=nonEnumerableProperties[i];if(key in map&&inArray(key,keys)<0){keys.push(key);}}
+keys.sort();for(i=0;i<keys.length;i++){key=keys[i];val=map[key];ret.push(dump.parse(key,"key")+": "+
+dump.parse(val,undefined,stack));}
+dump.down();return join("{",ret,"}");},node:function(node){var len,i,val,open=dump.HTML?"&lt;":"<",close=dump.HTML?"&gt;":">",tag=node.nodeName.toLowerCase(),ret=open+tag,attrs=node.attributes;if(attrs){for(i=0,len=attrs.length;i<len;i++){val=attrs[i].nodeValue;if(val&&val!=="inherit"){ret+=" "+attrs[i].nodeName+"="+
+dump.parse(val,"attribute");}}}
+ret+=close;if(node.nodeType===3||node.nodeType===4){ret+=node.nodeValue;}
+return ret+open+"/"+tag+close;},functionArgs:function(fn){var args,l=fn.length;if(!l){return "";}
+args=new Array(l);while(l--){args[l]=String.fromCharCode(97+l);}
+return " "+args.join(", ")+" ";},key:quote,functionCode:"[code]",attribute:quote,string:quote,date:quote,regexp:literal,number:literal,"boolean":literal},HTML:false,indentChar:" ",multiline:true};return dump;}());QUnit.jsDump=QUnit.dump;if(typeof window!=="undefined"){(function(){var i,assertions=Assert.prototype;function applyCurrent(current){return function(){var assert=new Assert(QUnit.config.current);current.apply(assert,arguments);};}
+for(i in assertions){QUnit[i]=applyCurrent(assertions[i]);}})();(function(){var i,l,keys=["test","module","expect","asyncTest","start","stop","ok","notOk","equal","notEqual","propEqual","notPropEqual","deepEqual","notDeepEqual","strictEqual","notStrictEqual","throws"];for(i=0,l=keys.length;i<l;i++){window[keys[i]]=QUnit[keys[i]];}})();window.QUnit=QUnit;}
+if(typeof module!=="undefined"&&module&&module.exports){module.exports=QUnit;module.exports.QUnit=QUnit;}
+if(typeof exports!=="undefined"&&exports){exports.QUnit=QUnit;}
+if(typeof define==="function"&&define.amd){define(function(){return QUnit;});QUnit.config.autostart=false;}}((function(){return this;})()));//
+/**/
+QUnit.diff=(function(){function DiffMatchPatch(){this.DiffTimeout=1.0;this.DiffEditCost=4;}
+var DIFF_DELETE=-1,DIFF_INSERT=1,DIFF_EQUAL=0;DiffMatchPatch.prototype.DiffMain=function(text1,text2,optChecklines,optDeadline){var deadline,checklines,commonlength,commonprefix,commonsuffix,diffs;if(typeof optDeadline==="undefined"){if(this.DiffTimeout<=0){optDeadline=Number.MAX_VALUE;}else{ optDeadline=(new Date()).getTime()+this.DiffTimeout*1000;}}
+deadline=optDeadline;if(text1===null||text2===null){throw new Error("Null input. (DiffMain)");}
+if(text1===text2){if(text1){return[[DIFF_EQUAL,text1]];}
+return[];}
+if(typeof optChecklines==="undefined"){optChecklines=true;}
+checklines=optChecklines;commonlength=this.diffCommonPrefix(text1,text2);commonprefix=text1.substring(0,commonlength);text1=text1.substring(commonlength);text2=text2.substring(commonlength);commonlength=this.diffCommonSuffix(text1,text2);commonsuffix=text1.substring(text1.length-commonlength);text1=text1.substring(0,text1.length-commonlength);text2=text2.substring(0,text2.length-commonlength);diffs=this.diffCompute(text1,text2,checklines,deadline);if(commonprefix){diffs.unshift([DIFF_EQUAL,commonprefix]);}
+if(commonsuffix){diffs.push([DIFF_EQUAL,commonsuffix]);}
+this.diffCleanupMerge(diffs);return diffs;};DiffMatchPatch.prototype.diffCleanupEfficiency=function(diffs){var changes,equalities,equalitiesLength,lastequality,pointer,preIns,preDel,postIns,postDel;changes=false;equalities=[];equalitiesLength=0;lastequality=null;pointer=0;preIns=false;preDel=false;postIns=false;postDel=false;while(pointer<diffs.length){if(diffs[pointer][0]===DIFF_EQUAL){if(diffs[pointer][1].length<this.DiffEditCost&&(postIns||postDel)){equalities[equalitiesLength++]=pointer;preIns=postIns;preDel=postDel;lastequality=diffs[pointer][1];}else{equalitiesLength=0;lastequality=null;}
+postIns=postDel=false;}else{if(diffs[pointer][0]===DIFF_DELETE){postDel=true;}else{postIns=true;}
+if(lastequality&&((preIns&&preDel&&postIns&&postDel)||((lastequality.length<this.DiffEditCost/2)&&(preIns+preDel+postIns+postDel)===3))){diffs.splice(equalities[equalitiesLength-1],0,[DIFF_DELETE,lastequality]);diffs[equalities[equalitiesLength-1]+1][0]=DIFF_INSERT;equalitiesLength--;lastequality=null;if(preIns&&preDel){postIns=postDel=true;equalitiesLength=0;}else{equalitiesLength--;pointer=equalitiesLength>0?equalities[equalitiesLength-1]:-1;postIns=postDel=false;}
+changes=true;}}
+pointer++;}
+if(changes){this.diffCleanupMerge(diffs);}};DiffMatchPatch.prototype.diffPrettyHtml=function(diffs){var op,data,x,html=[];for(x=0;x<diffs.length;x++){op=diffs[x][0];data=diffs[x][1];switch(op){case DIFF_INSERT:html[x]="<ins>"+data+"</ins>";break;case DIFF_DELETE:html[x]="<del>"+data+"</del>";break;case DIFF_EQUAL:html[x]="<span>"+data+"</span>";break;}}
+return html.join("");};DiffMatchPatch.prototype.diffCommonPrefix=function(text1,text2){var pointermid,pointermax,pointermin,pointerstart;if(!text1||!text2||text1.charAt(0)!==text2.charAt(0)){return 0;}
+pointermin=0;pointermax=Math.min(text1.length,text2.length);pointermid=pointermax;pointerstart=0;while(pointermin<pointermid){if(text1.substring(pointerstart,pointermid)===text2.substring(pointerstart,pointermid)){pointermin=pointermid;pointerstart=pointermin;}else{pointermax=pointermid;}
+pointermid=Math.floor((pointermax-pointermin)/2+pointermin);}
+return pointermid;};DiffMatchPatch.prototype.diffCommonSuffix=function(text1,text2){var pointermid,pointermax,pointermin,pointerend;if(!text1||!text2||text1.charAt(text1.length-1)!==text2.charAt(text2.length-1)){return 0;}
+pointermin=0;pointermax=Math.min(text1.length,text2.length);pointermid=pointermax;pointerend=0;while(pointermin<pointermid){if(text1.substring(text1.length-pointermid,text1.length-pointerend)===text2.substring(text2.length-pointermid,text2.length-pointerend)){pointermin=pointermid;pointerend=pointermin;}else{pointermax=pointermid;}
+pointermid=Math.floor((pointermax-pointermin)/2+pointermin);}
+return pointermid;};/**/
+DiffMatchPatch.prototype.diffCompute=function(text1,text2,checklines,deadline){var diffs,longtext,shorttext,i,hm,text1A,text2A,text1B,text2B,midCommon,diffsA,diffsB;if(!text1){return[[DIFF_INSERT,text2]];}
+if(!text2){return[[DIFF_DELETE,text1]];}
+longtext=text1.length>text2.length?text1:text2;shorttext=text1.length>text2.length?text2:text1;i=longtext.indexOf(shorttext);if(i!==-1){diffs=[[DIFF_INSERT,longtext.substring(0,i)],[DIFF_EQUAL,shorttext],[DIFF_INSERT,longtext.substring(i+shorttext.length)]];if(text1.length>text2.length){diffs[0][0]=diffs[2][0]=DIFF_DELETE;}
+return diffs;}
+if(shorttext.length===1){return[[DIFF_DELETE,text1],[DIFF_INSERT,text2]];}
+hm =this.diffHalfMatch(text1,text2);if(hm){text1A=hm[0];text1B=hm[1];text2A=hm[2];text2B=hm[3];midCommon=hm[4];diffsA=this.DiffMain(text1A,text2A,checklines,deadline);diffsB=this.DiffMain(text1B,text2B,checklines,deadline);return diffsA.concat([[DIFF_EQUAL,midCommon]],diffsB);}
+if(checklines&&text1.length>100&&text2.length>100){return this.diffLineMode(text1,text2,deadline);}
+return this.diffBisect(text1,text2,deadline);};DiffMatchPatch.prototype.diffHalfMatch=function(text1,text2){var longtext,shorttext,dmp,text1A,text2B,text2A,text1B,midCommon,hm1,hm2,hm;if(this.DiffTimeout<=0){return null;}
+longtext=text1.length>text2.length?text1:text2;shorttext=text1.length>text2.length?text2:text1;if(longtext.length<4||shorttext.length*2<longtext.length){return null;}
+dmp=this;/**/
+function diffHalfMatchI(longtext,shorttext,i){var seed,j,bestCommon,prefixLength,suffixLength,bestLongtextA,bestLongtextB,bestShorttextA,bestShorttextB;seed=longtext.substring(i,i+Math.floor(longtext.length/4));j=-1;bestCommon="";while((j=shorttext.indexOf(seed,j+1))!==-1){prefixLength=dmp.diffCommonPrefix(longtext.substring(i),shorttext.substring(j));suffixLength=dmp.diffCommonSuffix(longtext.substring(0,i),shorttext.substring(0,j));if(bestCommon.length<suffixLength+prefixLength){bestCommon=shorttext.substring(j-suffixLength,j)+
+shorttext.substring(j,j+prefixLength);bestLongtextA=longtext.substring(0,i-suffixLength);bestLongtextB=longtext.substring(i+prefixLength);bestShorttextA=shorttext.substring(0,j-suffixLength);bestShorttextB=shorttext.substring(j+prefixLength);}}
+if(bestCommon.length*2>=longtext.length){return[bestLongtextA,bestLongtextB,bestShorttextA,bestShorttextB,bestCommon
+];}else{return null;}}
+hm1=diffHalfMatchI(longtext,shorttext,Math.ceil(longtext.length/4));hm2=diffHalfMatchI(longtext,shorttext,Math.ceil(longtext.length/2));if(!hm1&&!hm2){return null;}else if(!hm2){hm=hm1;}else if(!hm1){hm=hm2;}else{hm=hm1[4].length>hm2[4].length?hm1:hm2;}
+text1A,text1B,text2A,text2B;if(text1.length>text2.length){text1A=hm[0];text1B=hm[1];text2A=hm[2];text2B=hm[3];}else{text2A=hm[0];text2B=hm[1];text1A=hm[2];text1B=hm[3];}
+midCommon=hm[4];return[text1A,text1B,text2A,text2B,midCommon];};DiffMatchPatch.prototype.diffLineMode=function(text1,text2,deadline){var a,diffs,linearray,pointer,countInsert,countDelete,textInsert,textDelete,j;a=this.diffLinesToChars(text1,text2);text1=a.chars1;text2=a.chars2;linearray=a.lineArray;diffs=this.DiffMain(text1,text2,false,deadline);
+this.diffCharsToLines(diffs,linearray);this.diffCleanupSemantic(diffs);diffs.push([DIFF_EQUAL,""]);pointer=0;countDelete=0;countInsert=0;textDelete="";textInsert="";while(pointer<diffs.length){switch(diffs[pointer][0]){case DIFF_INSERT:countInsert++;textInsert+=diffs[pointer][1];break;case DIFF_DELETE:countDelete++;textDelete+=diffs[pointer][1];break;case DIFF_EQUAL:if(countDelete>=1&&countInsert>=1){diffs.splice(pointer-countDelete-countInsert,countDelete+countInsert);pointer=pointer-countDelete-countInsert;a=this.DiffMain(textDelete,textInsert,false,deadline);for(j=a.length-1;j>=0;j--){diffs.splice(pointer,0,a[j]);}
+pointer=pointer+a.length;}
+countInsert=0;countDelete=0;textDelete="";textInsert="";break;}
+pointer++;}
+diffs.pop();return diffs;};DiffMatchPatch.prototype.diffBisect=function(text1,text2,deadline){var text1Length,text2Length,maxD,vOffset,vLength,v1,v2,x,delta,front,k1start,k1end,k2start,k2end,k2Offset,k1Offset,x1,x2,y1,y2,d,k1,k2;text1Length=text1.length;text2Length=text2.length;maxD=Math.ceil((text1Length+text2Length)/2);vOffset=maxD;vLength=2*maxD;v1=new Array(vLength);v2=new Array(vLength);for(x=0;x<vLength;x++){v1[x]=-1;v2[x]=-1;}
+ v1[vOffset+1]=0;v2[vOffset+1]=0;delta=text1Length-text2Length;front=(delta%2!==0);k1start=0;k1end=0;k2start=0;k2end=0;for(d=0;d<maxD;d++){if((new Date()).getTime()>deadline){break;}
+for(k1=-d+k1start;k1<=d-k1end;k1+=2){k1Offset=vOffset+k1;if(k1===-d||(k1!==d&&v1[k1Offset-1]<v1[k1Offset+1])){x1=v1[k1Offset+1];}else{x1=v1[k1Offset-1]+1;}
+y1=x1-k1;while(x1<text1Length&&y1<text2Length&&text1.charAt(x1)===text2.charAt(y1)){x1++;y1++;}
+v1[k1Offset]=x1;if(x1>text1Length){k1end+=2;}else if(y1>text2Length){k1start+=2;}else if(front){k2Offset=vOffset+delta-k1;if(k2Offset>=0&&k2Offset<vLength&&v2[k2Offset]!==-1){x2=text1Length-v2[k2Offset];if(x1>=x2){return this.diffBisectSplit(text1,text2,x1,y1,deadline);}}
+}}
+for(k2=-d+k2start;k2<=d-k2end;k2+=2){k2Offset=vOffset+k2;if(k2===-d||(k2!==d&&v2[k2Offset-1]<v2[k2Offset+1])){x2=v2[k2Offset+1];}else{x2=v2[k2Offset-1]+1;}
+y2=x2-k2;while(x2<text1Length&&y2<text2Length&&text1.charAt(text1Length-x2-1)===text2.charAt(text2Length-y2-1)){x2++;y2++;}
+v2[k2Offset]=x2;if(x2>text1Length){k2end+=2;}else if(y2>text2Length){k2start+=2;}else if(!front){k1Offset=vOffset+delta-k2;if(k1Offset>=0&&k1Offset<vLength&&v1[k1Offset]!==-1){x1=v1[k1Offset];y1=vOffset+x1-k1Offset;x2=text1Length-x2;if(x1>=x2){return this.diffBisectSplit(text1,text2,x1,y1,deadline);}}}}}
+return[[DIFF_DELETE,text1],[DIFF_INSERT,text2]];};/**/
+DiffMatchPatch.prototype.diffBisectSplit=function(text1,text2,x,y,deadline){var text1a,text1b,text2a,text2b,diffs,diffsb;text1a=text1.substring(0,x);text2a=text2.substring(0,y);text1b=text1.substring(x);text2b=text2.substring(y);diffs=this.DiffMain(text1a,text2a,false,deadline);diffsb=this.DiffMain(text1b,text2b,false,deadline);return diffs.concat(diffsb);};DiffMatchPatch.prototype.diffCleanupSemantic=function(diffs){var changes,equalities,equalitiesLength,lastequality,pointer,lengthInsertions2,lengthDeletions2,lengthInsertions1,lengthDeletions1,deletion,insertion,overlapLength1,overlapLength2;changes=false;equalities=[];equalitiesLength=0;lastequality=null;pointer=0;lengthInsertions1=0;lengthDeletions1=0;lengthInsertions2=0;lengthDeletions2=0;while(pointer<diffs.length){if(diffs[pointer][0]===DIFF_EQUAL){equalities[equalitiesLength++]=pointer;lengthInsertions1=lengthInsertions2;lengthDeletions1=lengthDeletions2;lengthInsertions2=0;lengthDeletions2=0;lastequality=diffs[pointer][1];}else{if(diffs[pointer][0]===DIFF_INSERT){lengthInsertions2+=diffs[pointer][1].length;}else{lengthDeletions2+=diffs[pointer][1].length;}
+if(lastequality&&(lastequality.length<=Math.max(lengthInsertions1,lengthDeletions1))&&(lastequality.length<=Math.max(lengthInsertions2,lengthDeletions2))){diffs.splice(equalities[equalitiesLength-1],0,[DIFF_DELETE,lastequality]);diffs[equalities[equalitiesLength-1]+1][0]=DIFF_INSERT;equalitiesLength--;equalitiesLength--;pointer=equalitiesLength>0?equalities[equalitiesLength-1]:-1;lengthInsertions1=0;lengthDeletions1=0;lengthInsertions2=0;lengthDeletions2=0;lastequality=null;changes=true;}}
+pointer++;}
+if(changes){this.diffCleanupMerge(diffs);}
+pointer=1;while(pointer<diffs.length){if(diffs[pointer-1][0]===DIFF_DELETE&&diffs[pointer][0]===DIFF_INSERT){deletion=diffs[pointer-1][1];insertion=diffs[pointer][1];overlapLength1=this.diffCommonOverlap(deletion,insertion);overlapLength2=this.diffCommonOverlap(insertion,deletion);if(overlapLength1>=overlapLength2){if(overlapLength1>=deletion.length/2||overlapLength1>=insertion.length/2){diffs.splice(pointer,0,[DIFF_EQUAL,insertion.substring(0,overlapLength1)]);diffs[pointer-1][1]=deletion.substring(0,deletion.length-overlapLength1);diffs[pointer+1][1]=insertion.substring(overlapLength1);pointer++;}}else{if(overlapLength2>=deletion.length/2||overlapLength2>=insertion.length/2){diffs.splice(pointer,0,[DIFF_EQUAL,deletion.substring(0,overlapLength2)]);diffs[pointer-1][0]=DIFF_INSERT;diffs[pointer-1][1]=insertion.substring(0,insertion.length-overlapLength2);diffs[pointer+1][0]=DIFF_DELETE;diffs[pointer+1][1]=deletion.substring(overlapLength2);pointer++;}}
+pointer++;}
+pointer++;}};DiffMatchPatch.prototype.diffCommonOverlap=function(text1,text2){var text1Length,text2Length,textLength,best,length,pattern,found;text1Length=text1.length;text2Length=text2.length;if(text1Length===0||text2Length===0){return 0;}
+if(text1Length>text2Length){text1=text1.substring(text1Length-text2Length);}else if(text1Length<text2Length){ text2=text2.substring(0,text1Length);}
+textLength=Math.min(text1Length,text2Length);if(text1===text2){return textLength;}
+best=0;length=1;while(true){pattern=text1.substring(textLength-length);found=text2.indexOf(pattern);if(found===-1){return best;}
+length+=found;if(found===0||text1.substring(textLength-length)===text2.substring(0,length)){best=length;length++;}}};/**/
+DiffMatchPatch.prototype.diffLinesToChars=function(text1,text2){var lineArray,lineHash,chars1,chars2;lineArray=[];lineHash={};lineArray[0]="";function diffLinesToCharsMunge(text){var chars,lineStart,lineEnd,lineArrayLength,line;chars="";lineStart=0;lineEnd=-1;lineArrayLength=lineArray.length;while(lineEnd<text.length-1){lineEnd=text.indexOf("\n",lineStart);if(lineEnd===-1){lineEnd=text.length-1;}
+line=text.substring(lineStart,lineEnd+1);lineStart=lineEnd+1;if(lineHash.hasOwnProperty?lineHash.hasOwnProperty(line):(lineHash[line]!==undefined)){chars+=String.fromCharCode(lineHash[line]);}else{chars+=String.fromCharCode(lineArrayLength);lineHash[line]=lineArrayLength;lineArray[lineArrayLength++]=line;}}
+return chars;}
+chars1=diffLinesToCharsMunge(text1);chars2=diffLinesToCharsMunge(text2);return{chars1:chars1,chars2:chars2,lineArray:lineArray};};DiffMatchPatch.prototype.diffCharsToLines=function(diffs,lineArray){var x,chars,text,y;for(x=0;x<diffs.length;x++){chars=diffs[x][1];text=[];for(y=0;y<chars.length;y++){text[y]=lineArray[chars.charCodeAt(y)];}
+diffs[x][1]=text.join("");}};DiffMatchPatch.prototype.diffCleanupMerge=function(diffs){var pointer,countDelete,countInsert,textInsert,textDelete,commonlength,changes;diffs.push([DIFF_EQUAL,""]);pointer=0;countDelete=0;countInsert=0;textDelete="";textInsert="";commonlength;while(pointer<diffs.length){switch(diffs[pointer][0]){case DIFF_INSERT:countInsert++;textInsert+=diffs[pointer][1];pointer++;break;case DIFF_DELETE:countDelete++;textDelete+=diffs[pointer][1];pointer++;break;case DIFF_EQUAL:if(countDelete+countInsert>1){if(countDelete!==0&&countInsert!==0){commonlength=this.diffCommonPrefix(textInsert,textDelete);if(commonlength!==0){if((pointer-countDelete-countInsert)>0&&diffs[pointer-countDelete-countInsert-1][0]===
+ DIFF_EQUAL){diffs[pointer-countDelete-countInsert-1][1]+=textInsert.substring(0,commonlength);}else{diffs.splice(0,0,[DIFF_EQUAL,textInsert.substring(0,commonlength)]);pointer++;}
+textInsert=textInsert.substring(commonlength);textDelete=textDelete.substring(commonlength);}
+commonlength=this.diffCommonSuffix(textInsert,textDelete);if(commonlength!==0){diffs[pointer][1]=textInsert.substring(textInsert.length-
+commonlength)+diffs[pointer][1];textInsert=textInsert.substring(0,textInsert.length-
+commonlength);textDelete=textDelete.substring(0,textDelete.length-
+commonlength);}}
+if(countDelete===0){diffs.splice(pointer-countInsert,countDelete+countInsert,[DIFF_INSERT,textInsert]);}else if(countInsert===0){diffs.splice(pointer-countDelete,countDelete+countInsert,[DIFF_DELETE,textDelete]);}else{diffs.splice(pointer-countDelete-countInsert, countDelete+countInsert,[DIFF_DELETE,textDelete],[DIFF_INSERT,textInsert]);}
+pointer=pointer-countDelete-countInsert+(countDelete?1:0)+(countInsert?1:0)+1;}else if(pointer!==0&&diffs[pointer-1][0]===DIFF_EQUAL){diffs[pointer-1][1]+=diffs[pointer][1];diffs.splice(pointer,1);}else{pointer++;}
+countInsert=0;countDelete=0;textDelete="";textInsert="";break;}}
+if(diffs[diffs.length-1][1]===""){diffs.pop();}
+changes=false;pointer=1;while(pointer<diffs.length-1){if(diffs[pointer-1][0]===DIFF_EQUAL&&diffs[pointer+1][0]===DIFF_EQUAL){if(diffs[pointer][1].substring(diffs[pointer][1].length-
+diffs[pointer-1][1].length)===diffs[pointer-1][1]){diffs[pointer][1]=diffs[pointer-1][1]+
+diffs[pointer][1].substring(0,diffs[pointer][1].length-
+diffs[pointer-1][1].length);diffs[pointer+1][1]=diffs[pointer-1][1]+diffs[pointer+1][1];diffs.splice(pointer-1,1);changes=true;}else if(diffs[pointer][1].substring(0,diffs[pointer+1][1].length)===diffs[pointer+1][1]){diffs[pointer-1][1]+=diffs[pointer+1][1];diffs[pointer][1]=diffs[pointer][1].substring(diffs[pointer+1][1].length)+
+diffs[pointer+1][1];diffs.splice(pointer+1,1);changes=true;}}
+pointer++;}
+if(changes){this.diffCleanupMerge(diffs);}};return function(o,n){var diff,output,text;diff=new DiffMatchPatch();output=diff.DiffMain(o,n);diff.diffCleanupEfficiency(output);text=diff.diffPrettyHtml(output);return text;};}());//
+(function(){QUnit.init=function(){var tests,banner,result,qunit,config=QUnit.config;config.stats={all:0,bad:0};config.moduleStats={all:0,bad:0};config.started=0;config.updateRate=1000;config.blocking=false;config.autostart=true;config.autorun=false;config.filter="";config.queue=[];if(typeof window==="undefined"){return;}
+qunit=id("qunit");if(qunit){qunit.innerHTML="<h1 id='qunit-header'>"+escapeText(document.title)+"</h1>"+
+"<h2 id='qunit-banner'></h2>"+
+"<div id='qunit-testrunner-toolbar'></div>"+
+"<h2 id='qunit-userAgent'></h2>"+
+"<ol id='qunit-tests'></ol>";}
+tests=id("qunit-tests");banner=id("qunit-banner");result=id("qunit-testresult");if(tests){tests.innerHTML="";}
+if(banner){banner.className="";}
+if(result){result.parentNode.removeChild(result);}
+if(tests){result=document.createElement("p");result.id="qunit-testresult";result.className="result";tests.parentNode.insertBefore(result,tests);result.innerHTML="Running...<br />&#160;";}};if(typeof window==="undefined"){return;}
+var config=QUnit.config,hasOwn=Object.prototype.hasOwnProperty,defined={document:window.document!==undefined,sessionStorage:(function(){var x="qunit-test-string";try{sessionStorage.setItem(x,x);sessionStorage.removeItem(x);return true;}catch(e){return false;}}())},modulesList=[];function escapeText(s){if(!s){return "";}
+s=s+"";return s.replace(/['"<>&]/g,function(s){switch(s){case "'":return "&#039;";case "\"":return "&quot;";case "<":return "&lt;";case ">":return "&gt;";case "&":return "&amp;";}});}
+function addEvent(elem,type,fn){if(elem.addEventListener){elem.addEventListener(type,fn,false);}else if(elem.attachEvent){elem.attachEvent("on"+type,function(){var event=window.event;if(!event.target){event.target=event.srcElement||document;}
+fn.call(elem,event);});}}
+function addEvents(elems,type,fn){var i=elems.length;while(i--){addEvent(elems[i],type,fn);}}
+function hasClass(elem,name){return(" "+elem.className+" ").indexOf(" "+name+" ")>=0;}
+function addClass(elem,name){if(!hasClass(elem,name)){elem.className+=(elem.className?" ":"")+name;}}
+function toggleClass(elem,name){if(hasClass(elem,name)){removeClass(elem,name);}else{addClass(elem,name);}}
+function removeClass(elem,name){var set=" "+elem.className+" ";while(set.indexOf(" "+name+" ")>=0){set=set.replace(" "+name+" "," ");}
+elem.className=typeof set.trim==="function"?set.trim():set.replace(/^\s+|\s+$/g,"");}
+function id(name){return defined.document&&document.getElementById&&document.getElementById(name);}
+function getUrlConfigHtml(){var i,j,val,escaped,escapedTooltip,selection=false,len=config.urlConfig.length,urlConfigHtml="";for(i=0;i<len;i++){val=config.urlConfig[i];if(typeof val==="string"){val={id:val,label:val};}
+escaped=escapeText(val.id);escapedTooltip=escapeText(val.tooltip);if(config[val.id]===undefined){config[val.id]=QUnit.urlParams[val.id];}
+if(!val.value||typeof val.value==="string"){urlConfigHtml+="<input id='qunit-urlconfig-"+escaped+
+"' name='"+escaped+"' type='checkbox'"+(val.value?" value='"+escapeText(val.value)+"'":"")+(config[val.id]?" checked='checked'":"")+
+" title='"+escapedTooltip+"' /><label for='qunit-urlconfig-"+escaped+
+"' title='"+escapedTooltip+"'>"+val.label+"</label>";}else{urlConfigHtml+="<label for='qunit-urlconfig-"+escaped+
+"' title='"+escapedTooltip+"'>"+val.label+
+": </label><select id='qunit-urlconfig-"+escaped+
+"' name='"+escaped+"' title='"+escapedTooltip+"'><option></option>";if(QUnit.is("array",val.value)){for(j=0;j<val.value.length;j++){escaped=escapeText(val.value[j]);urlConfigHtml+="<option value='"+escaped+"'"+(config[val.id]===val.value[j]?(selection=true)&&" selected='selected'":"")+
+">"+escaped+"</option>";}}else{for(j in val.value){if(hasOwn.call(val.value,j)){urlConfigHtml+="<option value='"+escapeText(j)+"'"+(config[val.id]===j?(selection=true)&&" selected='selected'":"")+
+">"+escapeText(val.value[j])+"</option>";}}}
+if(config[val.id]&&!selection){escaped=escapeText(config[val.id]);urlConfigHtml+="<option value='"+escaped+
+"' selected='selected' disabled='disabled'>"+escaped+"</option>";}
+urlConfigHtml+="</select>";}}
+return urlConfigHtml;}
+function toolbarChanged(){var updatedUrl,value,field=this,params={};//
+if("selectedIndex" in field){value=field.options[field.selectedIndex].value||undefined;}else{value=field.checked?(field.defaultValue||true):undefined;}
+params[field.name]=value;updatedUrl=setUrl(params);if("hidepassed"===field.name&&"replaceState" in window.history){config[field.name]=value||false;if(value){addClass(id("qunit-tests"),"hidepass");}else{removeClass(id("qunit-tests"),"hidepass");}
+window.history.replaceState(null,"",updatedUrl);}else{window.location=updatedUrl;}}
+function setUrl(params){var key,querystring="?";params=QUnit.extend(QUnit.extend({},QUnit.urlParams),params);for(key in params){if(hasOwn.call(params,key)){if(params[key]===undefined){continue;}
+querystring+=encodeURIComponent(key);if(params[key]!==true){querystring+="="+encodeURIComponent(params[key]);}
+querystring+="&";}}
+return location.protocol+"//"+location.host+
+location.pathname+querystring.slice(0,-1);}
+function applyUrlParams(){var selectedModule,modulesList=id("qunit-modulefilter"),filter=id("qunit-filter-input").value;selectedModule=modulesList?decodeURIComponent(modulesList.options[modulesList.selectedIndex].value):undefined;window.location=setUrl({module:(selectedModule==="")?undefined:selectedModule,filter:(filter==="")?undefined:filter,testId:undefined});}
+function toolbarUrlConfigContainer(){var urlConfigContainer=document.createElement("span");urlConfigContainer.innerHTML=getUrlConfigHtml();addClass(urlConfigContainer,"qunit-url-config");addEvents(urlConfigContainer.getElementsByTagName("input"),"click",toolbarChanged);addEvents(urlConfigContainer.getElementsByTagName("select"),"change",toolbarChanged);return urlConfigContainer;}
+function toolbarLooseFilter(){var filter=document.createElement("form"),label=document.createElement("label"),input=document.createElement("input"),button=document.createElement("button");addClass(filter,"qunit-filter");label.innerHTML="Filter: ";input.type="text";input.value=config.filter||"";input.name="filter";input.id="qunit-filter-input";button.innerHTML="Go";label.appendChild(input);filter.appendChild(label);filter.appendChild(button);addEvent(filter,"submit",function(ev){applyUrlParams();if(ev&&ev.preventDefault){ev.preventDefault();}
+return false;});return filter;}
+function toolbarModuleFilterHtml(){var i,moduleFilterHtml="";if(!modulesList.length){return false;}
+modulesList.sort(function(a,b){return a.localeCompare(b);});moduleFilterHtml+="<label for='qunit-modulefilter'>Module: </label>"+
+"<select id='qunit-modulefilter' name='modulefilter'><option value='' "+(QUnit.urlParams.module===undefined?"selected='selected'":"")+
+">< All Modules ></option>";for(i=0;i<modulesList.length;i++){moduleFilterHtml+="<option value='"+
+escapeText(encodeURIComponent(modulesList[i]))+"' "+(QUnit.urlParams.module===modulesList[i]?"selected='selected'":"")+
+">"+escapeText(modulesList[i])+"</option>";}
+moduleFilterHtml+="</select>";return moduleFilterHtml;}
+function toolbarModuleFilter(){var toolbar=id("qunit-testrunner-toolbar"),moduleFilter=document.createElement("span"),moduleFilterHtml=toolbarModuleFilterHtml();if(!toolbar||!moduleFilterHtml){return false;}
+moduleFilter.setAttribute("id","qunit-modulefilter-container");moduleFilter.innerHTML=moduleFilterHtml;addEvent(moduleFilter.lastChild,"change",applyUrlParams);toolbar.appendChild(moduleFilter);}
+function appendToolbar(){var toolbar=id("qunit-testrunner-toolbar");if(toolbar){toolbar.appendChild(toolbarUrlConfigContainer());toolbar.appendChild(toolbarLooseFilter());}}
+function appendHeader(){var header=id("qunit-header");if(header){header.innerHTML="<a href='"+
+setUrl({filter:undefined,module:undefined,testId:undefined})+
+"'>"+header.innerHTML+"</a> ";}}
+function appendBanner(){var banner=id("qunit-banner");if(banner){banner.className="";}}
+function appendTestResults(){var tests=id("qunit-tests"),result=id("qunit-testresult");if(result){result.parentNode.removeChild(result);}
+if(tests){tests.innerHTML="";result=document.createElement("p");result.id="qunit-testresult";result.className="result";tests.parentNode.insertBefore(result,tests);result.innerHTML="Running...<br />&#160;";}}
+function storeFixture(){var fixture=id("qunit-fixture");if(fixture){config.fixture=fixture.innerHTML;}}
+function appendUserAgent(){var userAgent=id("qunit-userAgent");if(userAgent){userAgent.innerHTML="";userAgent.appendChild(document.createTextNode("QUnit "+QUnit.version+"; "+navigator.userAgent));}}
+function appendTestsList(modules){var i,l,x,z,test,moduleObj;for(i=0,l=modules.length;i<l;i++){moduleObj=modules[i];if(moduleObj.name){modulesList.push(moduleObj.name);}
+for(x=0,z=moduleObj.tests.length;x<z;x++){test=moduleObj.tests[x];appendTest(test.name,test.testId,moduleObj.name);}}}
+function appendTest(name,testId,moduleName){var title,rerunTrigger,testBlock,assertList,tests=id("qunit-tests");if(!tests){return;}
+title=document.createElement("strong");title.innerHTML=getNameHtml(name,moduleName);rerunTrigger=document.createElement("a");rerunTrigger.innerHTML="Rerun";rerunTrigger.href=setUrl({testId:testId});testBlock=document.createElement("li");testBlock.appendChild(title);testBlock.appendChild(rerunTrigger);testBlock.id="qunit-test-output-"+testId;assertList=document.createElement("ol");assertList.className="qunit-assert-list";testBlock.appendChild(assertList);tests.appendChild(testBlock);}
+QUnit.begin(function(details){var qunit=id("qunit");storeFixture();if(qunit){qunit.innerHTML="<h1 id='qunit-header'>"+escapeText(document.title)+"</h1>"+
+"<h2 id='qunit-banner'></h2>"+
+"<div id='qunit-testrunner-toolbar'></div>"+
+"<h2 id='qunit-userAgent'></h2>"+
+"<ol id='qunit-tests'></ol>";}
+appendHeader();appendBanner();appendTestResults();appendUserAgent();appendToolbar();appendTestsList(details.modules);toolbarModuleFilter();if(qunit&&config.hidepassed){addClass(qunit.lastChild,"hidepass");}});QUnit.done(function(details){var i,key,banner=id("qunit-banner"),tests=id("qunit-tests"),html=["Tests completed in ",details.runtime," milliseconds.<br />","<span class='passed'>",details.passed,"</span> assertions of <span class='total'>",details.total,"</span> passed, <span class='failed'>",details.failed,"</span> failed."].join("");if(banner){banner.className=details.failed?"qunit-fail":"qunit-pass";}
+if(tests){id("qunit-testresult").innerHTML=html;}
+if(config.altertitle&&defined.document&&document.title){document.title=[(details.failed?"\u2716":"\u2714"),document.title.replace(/^[\u2714\u2716] /i,"")].join(" ");}
+if(config.reorder&&defined.sessionStorage&&details.failed===0){for(i=0;i<sessionStorage.length;i++){key=sessionStorage.key(i++);if(key.indexOf("qunit-test-")===0){sessionStorage.removeItem(key);}}}
+if(config.scrolltop&&window.scrollTo){window.scrollTo(0,0);}});function getNameHtml(name,module){var nameHtml="";if(module){nameHtml="<span class='module-name'>"+escapeText(module)+"</span>: ";}
+nameHtml+="<span class='test-name'>"+escapeText(name)+"</span>";return nameHtml;}
+QUnit.testStart(function(details){var running,testBlock,bad;testBlock=id("qunit-test-output-"+details.testId);if(testBlock){testBlock.className="running";}else{appendTest(details.name,details.testId,details.module);}
+running=id("qunit-testresult");if(running){bad=QUnit.config.reorder&&defined.sessionStorage&&+sessionStorage.getItem("qunit-test-"+details.module+"-"+details.name);running.innerHTML=(bad?"Rerunning previously failed test: <br />":"Running: <br />")+
+getNameHtml(details.name,details.module);}});QUnit.log(function(details){var assertList,assertLi,message,expected,actual,testItem=id("qunit-test-output-"+details.testId);if(!testItem){return;}
+message=escapeText(details.message)||(details.result?"okay":"failed");message="<span class='test-message'>"+message+"</span>";message+="<span class='runtime'>@ "+details.runtime+" ms</span>";if(!details.result&&hasOwn.call(details,"expected")){expected=escapeText(QUnit.dump.parse(details.expected));actual=escapeText(QUnit.dump.parse(details.actual));message+="<table><tr class='test-expected'><th>Expected: </th><td><pre>"+
+expected+
+"</pre></td></tr>";if(actual!==expected){message+="<tr class='test-actual'><th>Result: </th><td><pre>"+
+actual+"</pre></td></tr>"+
+"<tr class='test-diff'><th>Diff: </th><td><pre>"+
+QUnit.diff(expected,actual)+"</pre></td></tr>";}else{if(expected.indexOf("[object Array]")!==-1||expected.indexOf("[object Object]")!==-1){message+="<tr class='test-message'><th>Message: </th><td>"+
+"Diff suppressed as the depth of object is more than current max depth ("+
+QUnit.config.maxDepth+").<p>Hint: Use <code>QUnit.dump.maxDepth</code> to "+
+" run with a higher max depth or <a href='"+setUrl({maxDepth:-1})+"'>"+
+"Rerun</a> without max depth.</p></td></tr>";}}
+if(details.source){message+="<tr class='test-source'><th>Source: </th><td><pre>"+
+escapeText(details.source)+"</pre></td></tr>";}
+message+="</table>";}else if(!details.result&&details.source){message+="<table>"+
+"<tr class='test-source'><th>Source: </th><td><pre>"+
+escapeText(details.source)+"</pre></td></tr>"+
+"</table>";}
+assertList=testItem.getElementsByTagName("ol")[0];assertLi=document.createElement("li");assertLi.className=details.result?"pass":"fail";assertLi.innerHTML=message;assertList.appendChild(assertLi);});QUnit.testDone(function(details){var testTitle,time,testItem,assertList,good,bad,testCounts,skipped,tests=id("qunit-tests");if(!tests){return;}
+testItem=id("qunit-test-output-"+details.testId);assertList=testItem.getElementsByTagName("ol")[0];good=details.passed;bad=details.failed;if(config.reorder&&defined.sessionStorage){if(bad){sessionStorage.setItem("qunit-test-"+details.module+"-"+details.name,bad);}else{sessionStorage.removeItem("qunit-test-"+ details.module+"-"+details.name);}}
+if(bad===0){addClass(assertList,"qunit-collapsed");}
+testTitle=testItem.firstChild;testCounts=bad?"<b class='failed'>"+bad+"</b>, "+"<b class='passed'>"+good+"</b>, ":"";testTitle.innerHTML+=" <b class='counts'>("+testCounts+
+details.assertions.length+")</b>";if(details.skipped){testItem.className="skipped";skipped=document.createElement("em");skipped.className="qunit-skipped-label";skipped.innerHTML="skipped";testItem.insertBefore(skipped,testTitle);}else{addEvent(testTitle,"click",function(){toggleClass(assertList,"qunit-collapsed");});testItem.className=bad?"fail":"pass";time=document.createElement("span");time.className="runtime";time.innerHTML=details.runtime+" ms";testItem.insertBefore(time,assertList);}});if(defined.document){if(document.readyState==="complete"){QUnit.load();}else{addEvent(window,"load",QUnit.load);}}else{config.pageLoaded=true;config.autorun=true;}})(); \ No newline at end of file
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/recipedetails.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/recipedetails.js
new file mode 100644
index 000000000..604db5f03
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/recipedetails.js
@@ -0,0 +1,51 @@
+"use strict";
+
+function recipeDetailsPageInit(ctx){
+
+ $(".customise-btn").click(function(e){
+ e.preventDefault();
+ var imgCustomModal = $("#new-custom-image-modal");
+
+ if (imgCustomModal.length === 0)
+ throw("Modal new-custom-image not found");
+
+ var recipe = {id: $(this).data('recipe'), name: null}
+ newCustomImageModalSetRecipes([recipe]);
+ imgCustomModal.modal('show');
+ });
+
+ $("#add-layer-btn").click(function(){
+ var btn = $(this);
+
+ libtoaster.addRmLayer(ctx.recipe.layer_version,
+ true,
+ function (layersList){
+ var msg = libtoaster.makeLayerAddRmAlertMsg(ctx.recipe.layer_version,
+ layersList,
+ true);
+
+ libtoaster.showChangeNotification(msg);
+
+ var toShow = $("#customise-build-btns");
+
+ /* If we have no packages built yet also fade in the build packages
+ * hint message
+ */
+ if (ctx.recipe.totalPackages === 0){
+ toShow = toShow.add("#build-to-get-packages-msg");
+ }
+
+ $("#packages-alert").add(btn).fadeOut(function(){
+ toShow.fadeIn();
+ });
+ });
+ });
+
+ /* Trigger a build of your custom image */
+ $(".build-recipe-btn").click(function(){
+ libtoaster.startABuild(null, ctx.recipe.name,
+ function(){
+ window.location.replace(libtoaster.ctx.projectBuildsUrl);
+ });
+ });
+}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/table.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/table.js
index 40b5022de..f738144ae 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -33,6 +33,10 @@ function tableInit(ctx){
loadData(tableParams);
+ // clicking on this set of elements removes the search
+ var clearSearchElements = $('.remove-search-btn-'+ctx.tableName +
+ ', .show-all-'+ctx.tableName);
+
function loadData(tableParams){
$.ajax({
type: "GET",
@@ -62,31 +66,27 @@ function tableInit(ctx){
paginationBtns.html("");
if (tableParams.search)
- $('.remove-search-btn-'+ctx.tableName).show();
+ clearSearchElements.show();
else
- $('.remove-search-btn-'+ctx.tableName).hide();
+ clearSearchElements.hide();
$('.table-count-' + ctx.tableName).text(tableData.total);
tableTotal = tableData.total;
if (tableData.total === 0){
tableContainer.hide();
- /* If we were searching show the new search bar and return */
- if (tableParams.search){
+ if ($("#no-results-special-"+ctx.tableName).length > 0) {
+ /* use this page's special no-results form instead of the default */
+ $("#no-results-search-input-"+ctx.tableName).val(tableParams.search);
+ $("#no-results-special-"+ctx.tableName).show();
+ $("#results-found-"+ctx.tableName).hide();
+ } else {
$("#new-search-input-"+ctx.tableName).val(tableParams.search);
$("#no-results-"+ctx.tableName).show();
}
table.trigger("table-done", [tableData.total, tableParams]);
return;
-
- /* We don't want to clutter the place with the table chrome if there
- * are only a few results */
- } else if (tableData.total <= 10 &&
- !tableParams.filter &&
- !tableParams.search){
- $("#table-chrome-"+ctx.tableName).hide();
- pagination.hide();
} else {
tableContainer.show();
$("#no-results-"+ctx.tableName).hide();
@@ -239,16 +239,19 @@ function tableInit(ctx){
}
}
+ if (col.field_name === tableData.default_orderby){
+ title.addClass("default-orderby");
+ }
+
} else {
/* Not orderable */
- header.addClass("muted");
header.css("font-weight", "normal");
- header.append(col.title+' ');
+ header.append('<span class="muted">' + col.title + '</span> ');
}
/* Setup the filter button */
if (col.filter_name){
- var filterBtn = $('<a href="#" role="button" class="pull-right btn btn-mini" data-toggle="modal"><i class="icon-filter filtered"></i></a>');
+ var filterBtn = $('<a href="#" role="button" data-filter-on="' + col.filter_name + '" class="pull-right btn btn-mini" data-toggle="modal"><i class="icon-filter filtered"></i></a>');
filterBtn.data('filter-name', col.filter_name);
filterBtn.prop('id', col.filter_name);
@@ -324,7 +327,7 @@ function tableInit(ctx){
$("#"+ctx.tableName+" th").each(function(){
for (var i in cols_hidden){
if ($(this).hasClass(cols_hidden[i])){
- $("."+cols_hidden[i]).hide();
+ table.find("."+cols_hidden[i]).hide();
$("#checkbox-"+cols_hidden[i]).removeAttr("checked");
}
}
@@ -334,13 +337,14 @@ function tableInit(ctx){
* user setting.
*/
for (var i in defaultHiddenCols) {
- $("."+defaultHiddenCols[i]).hide();
+ table.find("."+defaultHiddenCols[i]).hide();
$("#checkbox-"+defaultHiddenCols[i]).removeAttr("checked");
}
}
}
- function sortColumnClicked(){
+ function sortColumnClicked(e){
+ e.preventDefault();
/* We only have one sort at a time so remove any existing sort indicators */
$("#"+ctx.tableName+" th .icon-caret-down").hide();
@@ -377,14 +381,14 @@ function tableInit(ctx){
var disabled_cols = [];
if ($(this).prop("checked")) {
- $("."+col).show();
+ table.find("."+col).show();
} else {
- $("."+col).hide();
+ table.find("."+col).hide();
/* If we're ordered by the column we're hiding remove the order by */
if (col === tableParams.orderby ||
'-' + col === tableParams.orderby){
tableParams.orderby = null;
- loadData(tableParams);
+ $("#"+ctx.tableName +" .default-orderby").click();
}
}
@@ -396,11 +400,182 @@ function tableInit(ctx){
$.cookie("cols", JSON.stringify(disabled_cols));
}
+ /**
+ * Create the DOM/JS for the client side of a TableFilterActionToggle
+ * or TableFilterActionDay
+ *
+ * filterName: (string) internal name for the filter action
+ * filterActionData: (object)
+ * filterActionData.count: (number) The number of items this filter will
+ * show when selected
+ *
+ * NB this triggers a filtervalue event each time its radio button is checked
+ */
+ function createActionRadio(filterName, filterActionData) {
+ var hasNoRecords = (Number(filterActionData.count) == 0);
+
+ var actionStr = '<div class="radio">' +
+ '<input type="radio" name="filter"' +
+ ' value="' + filterName + '"';
+
+ if (hasNoRecords) {
+ actionStr += ' disabled="disabled"';
+ }
+
+ actionStr += ' id="' + filterName + '">' +
+ '<input type="hidden" name="filter_value" value="on"' +
+ ' data-value-for="' + filterName + '">' +
+ '<label class="filter-title' +
+ (hasNoRecords ? ' muted' : '') + '"' +
+ ' for="' + filterName + '">' +
+ filterActionData.title +
+ ' (' + filterActionData.count + ')' +
+ '</label>' +
+ '</div>';
+
+ var action = $(actionStr);
+
+ // fire the filtervalue event from this action when the radio button
+ // is active so that the apply button can be enabled
+ action.find('[type="radio"]').change(function () {
+ if ($(this).is(':checked')) {
+ action.trigger('filtervalue', 'on');
+ }
+ });
+
+ return action;
+ }
+
+ /**
+ * Create the DOM/JS for the client side of a TableFilterActionDateRange
+ *
+ * filterName: (string) internal name for the filter action
+ * filterValue: (string) from,to date range in format yyyy-mm-dd,yyyy-mm-dd;
+ * used to select the current values for the from/to datepickers;
+ * if this is partial (e.g. "yyyy-mm-dd,") only the applicable datepicker
+ * will have a date pre-selected; if empty, neither will
+ * filterActionData: (object) data for generating the action's HTML
+ * filterActionData.title: label for the radio button
+ * filterActionData.max: (string) maximum date for the pickers, in ISO 8601
+ * datetime format
+ * filterActionData.min: (string) minimum date for the pickers, ISO 8601
+ * datetime
+ *
+ * NB this triggers a filtervalue event each time its radio button is checked
+ */
+ function createActionDateRange(filterName, filterValue, filterActionData) {
+ var action = $('<div class="radio">' +
+ '<input type="radio" name="filter"' +
+ ' value="' + filterName + '" ' +
+ ' id="' + filterName + '">' +
+ '<input type="hidden" name="filter_value" value=""' +
+ ' data-value-for="' + filterName + '">' +
+ '<label class="filter-title"' +
+ ' for="' + filterName + '">' +
+ filterActionData.title +
+ '</label>' +
+ '<input type="text" maxlength="10" class="input-small"' +
+ ' data-date-from-for="' + filterName + '">' +
+ '<span class="help-inline">to</span>' +
+ '<input type="text" maxlength="10" class="input-small"' +
+ ' data-date-to-for="' + filterName + '">' +
+ '<span class="help-inline get-help">(yyyy-mm-dd)</span>' +
+ '</div>');
+
+ var radio = action.find('[type="radio"]');
+ var value = action.find('[data-value-for]');
+
+ // make the datepickers for the range
+ var options = {
+ dateFormat: 'yy-mm-dd',
+ maxDate: new Date(filterActionData.max),
+ minDate: new Date(filterActionData.min)
+ };
+
+ // create date pickers, setting currently-selected from and to dates
+ var selectedFrom = null;
+ var selectedTo = null;
+
+ var selectedFromAndTo = [];
+ if (filterValue) {
+ selectedFromAndTo = filterValue.split(',');
+ }
+
+ if (selectedFromAndTo.length == 2) {
+ selectedFrom = selectedFromAndTo[0];
+ selectedTo = selectedFromAndTo[1];
+ }
+
+ options.defaultDate = selectedFrom;
+ var inputFrom =
+ action.find('[data-date-from-for]').datepicker(options);
+ inputFrom.val(selectedFrom);
+
+ options.defaultDate = selectedTo;
+ var inputTo =
+ action.find('[data-date-to-for]').datepicker(options);
+ inputTo.val(selectedTo);
+
+ // set filter_value based on date pickers when
+ // one of their values changes; if either from or to are unset,
+ // the new value is null;
+ // this triggers a 'filter_value-change' event on the action's element,
+ // which is used to determine the disabled/enabled state of the "Apply"
+ // button
+ var changeHandler = function () {
+ var fromValue = inputFrom.val();
+ var toValue = inputTo.val();
+
+ var newValue = undefined;
+ if (fromValue !== '' && toValue !== '') {
+ newValue = fromValue + ',' + toValue;
+ }
+
+ value.val(newValue);
+
+ // if this action is selected, fire an event for the new range
+ if (radio.is(':checked')) {
+ action.trigger('filtervalue', newValue);
+ }
+ };
+
+ inputFrom.change(changeHandler);
+ inputTo.change(changeHandler);
+
+ // check the associated radio button on clicking a date picker
+ var checkRadio = function () {
+ radio.prop('checked', 'checked');
+
+ // checking the radio button this way doesn't cause the "change"
+ // event to fire, so we manually call the changeHandler
+ changeHandler();
+ };
+
+ inputFrom.focus(checkRadio);
+ inputTo.focus(checkRadio);
+
+ // selecting a date in a picker constrains the date you can
+ // set in the other picker
+ inputFrom.change(function () {
+ inputTo.datepicker('option', 'minDate', inputFrom.val());
+ });
+
+ inputTo.change(function () {
+ inputFrom.datepicker('option', 'maxDate', inputTo.val());
+ });
+
+ // checking the radio input causes the "Apply" button disabled state to
+ // change, depending on which from/to dates are supplied
+ radio.change(changeHandler);
+
+ return action;
+ }
+
function filterOpenClicked(){
var filterName = $(this).data('filter-name');
- /* We need to pass in the curren search so that the filter counts take
- * into account the current search filter
+ /* We need to pass in the current search so that the filter counts take
+ * into account the current search term
*/
var params = {
'name' : filterName,
@@ -414,38 +589,92 @@ function tableInit(ctx){
data: params,
headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
success: function (filterData) {
- var filterActionRadios = $('#filter-actions-'+ctx.tableName);
+ /*
+ filterData structure:
+
+ {
+ title: '<title for the filter popup>',
+ filter_actions: [
+ {
+ title: '<label for radio button inside the popup>',
+ name: '<name of the filter action>',
+ count: <number of items this filter will show>,
+ ... additional data for the action ...
+ }
+ ]
+ }
- $('#filter-modal-title-'+ctx.tableName).text(filterData.title);
+ each filter_action gets a radio button; the value of this is
+ set to filterName + ':' + filter_action.name; e.g.
- filterActionRadios.text("");
+ in_current_project:in_project
- for (var i in filterData.filter_actions){
- var filterAction = filterData.filter_actions[i];
+ specifies the "in_project" action of the "in_current_project"
+ filter
- var action = $('<label class="radio"><input type="radio" name="filter" value=""><span class="filter-title"></span></label>');
- var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
+ the filterName is set on the column filter icon, and corresponds
+ to a value in the table's filter map
- var radioInput = action.children("input");
+ when the filter popup's "Apply" button is clicked, the
+ value for the radio button which is checked is passed in the
+ querystring, along with a filter_value, and applied to the
+ queryset on the table
+ */
+ var filterActionRadios = $('#filter-actions-' + ctx.tableName);
+ var filterApplyBtn = $('[data-role="filter-apply"]');
- if (Number(filterAction.count) == 0){
- radioInput.attr("disabled", "disabled");
+ var setApplyButtonState = function (e, filterActionValue) {
+ if (filterActionValue !== undefined) {
+ filterApplyBtn.removeAttr('disabled');
}
+ else {
+ filterApplyBtn.attr('disabled', 'disabled');
+ }
+ };
+
+ $('#filter-modal-title-' + ctx.tableName).text(filterData.title);
- action.children(".filter-title").text(actionTitle);
+ filterActionRadios.empty();
- radioInput.val(filterName + ':' + filterAction.name);
+ // create a radio button + form elements for each action associated
+ // with the filter on this column of the table
+ for (var i in filterData.filter_actions) {
+ var action = null;
+ var filterActionData = filterData.filter_actions[i];
+ var filterName = filterData.name + ':' +
+ filterActionData.action_name;
- /* Setup the current selected filter, default to 'all' if
- * no current filter selected.
- */
- if ((tableParams.filter &&
- tableParams.filter === radioInput.val()) ||
- filterAction.name == 'all') {
- radioInput.attr("checked", "checked");
+ if (filterActionData.type === 'toggle' ||
+ filterActionData.type === 'day') {
+ action = createActionRadio(filterName, filterActionData);
+ }
+ else if (filterActionData.type === 'daterange') {
+ // current values for the from/to dates
+ var filterValue = tableParams.filter_value;
+
+ action = createActionDateRange(
+ filterName,
+ filterValue,
+ filterActionData
+ );
}
- filterActionRadios.append(action);
+ if (action) {
+ // Setup the current selected filter; default to 'all' if
+ // no current filter selected
+ var radioInput = action.children('input[name="filter"]');
+ if ((tableParams.filter &&
+ tableParams.filter === radioInput.val()) ||
+ filterActionData.action_name == 'all') {
+ radioInput.prop("checked", "checked");
+ }
+
+ filterActionRadios.append(action);
+
+ // if the action's filter_value changes but is falsy, disable
+ // the "Apply" button
+ action.on('filtervalue', setApplyButtonState);
+ }
}
$('#filter-modal-'+ctx.tableName).modal('show');
@@ -453,6 +682,13 @@ function tableInit(ctx){
});
}
+ /* Allow pages to trigger reload event */
+ table.on('reload', function(e, newTableParams){
+ if (newTableParams)
+ loadData(newTableParams);
+ else
+ loadData(tableParams)
+ });
$(".get-help").tooltip({container:'body', html:true, delay:{show:300}});
@@ -476,6 +712,7 @@ function tableInit(ctx){
});
$("#search-submit-"+ctx.tableName).click(function(e){
+ e.preventDefault();
var searchTerm = $("#search-input-"+ctx.tableName).val();
tableParams.page = 1;
@@ -489,11 +726,9 @@ function tableInit(ctx){
}
loadData(tableParams);
-
- e.preventDefault();
});
- $('.remove-search-btn-'+ctx.tableName).click(function(e){
+ clearSearchElements.click(function(e){
e.preventDefault();
tableParams.page = 1;
@@ -514,7 +749,9 @@ function tableInit(ctx){
e.preventDefault();
});
- $("#clear-filter-btn-"+ctx.tableName).click(function(){
+ $("#clear-filter-btn-"+ctx.tableName).click(function(e){
+ e.preventDefault();
+
var filterBtn = $("#" + tableParams.filter.split(":")[0]);
filterBtnActive(filterBtn, false);
@@ -525,15 +762,27 @@ function tableInit(ctx){
$("#filter-modal-form-"+ctx.tableName).submit(function(e){
e.preventDefault();
- tableParams.filter = $(this).find("input[type='radio']:checked").val();
+ /* remove active status from all filter buttons so that only one filter
+ can be active at a time */
+ $('[data-filter-on]').each(function (index, filterBtn) {
+ filterBtnActive($(filterBtn), false);
+ });
- var filterBtn = $("#" + tableParams.filter.split(":")[0]);
+ // checked radio button
+ var checkedFilter = $(this).find("input[name='filter']:checked");
+ tableParams.filter = checkedFilter.val();
+
+ // hidden field holding the value for the checked filter
+ var checkedFilterValue = $(this).find("input[data-value-for='" +
+ tableParams.filter + "']");
+ tableParams.filter_value = checkedFilterValue.val();
/* All === remove filter */
if (tableParams.filter.match(":all$")) {
tableParams.filter = null;
- filterBtnActive(filterBtn, false);
+ tableParams.filter_value = null;
} else {
+ var filterBtn = $("#" + tableParams.filter.split(":")[0]);
filterBtnActive(filterBtn, true);
}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/tablefilter.py b/yocto-poky/bitbake/lib/toaster/toastergui/tablefilter.py
new file mode 100644
index 000000000..9d15bcff0
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/tablefilter.py
@@ -0,0 +1,292 @@
+#
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2015 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.db.models import Q, Max, Min
+from django.utils import dateparse, timezone
+from datetime import timedelta
+
+class TableFilter(object):
+ """
+ Stores a filter for a named field, and can retrieve the action
+ requested from the set of actions for that filter;
+ the order in which actions are added governs the order in which they
+ are returned in the JSON for the filter
+ """
+
+ def __init__(self, name, title):
+ self.name = name
+ self.title = title
+ self.__filter_action_map = {}
+
+ # retains the ordering of actions
+ self.__filter_action_keys = []
+
+ def add_action(self, action):
+ self.__filter_action_keys.append(action.name)
+ self.__filter_action_map[action.name] = action
+
+ def get_action(self, action_name):
+ return self.__filter_action_map[action_name]
+
+ def to_json(self, queryset):
+ """
+ Dump all filter actions as an object which can be JSON serialised;
+ this is used to generate the JSON for processing in
+ table.js / filterOpenClicked()
+ """
+ filter_actions = []
+
+ # add the "all" pseudo-filter action, which just selects the whole
+ # queryset
+ filter_actions.append({
+ 'action_name' : 'all',
+ 'title' : 'All',
+ 'type': 'toggle',
+ 'count' : queryset.count()
+ })
+
+ # add other filter actions
+ for action_name in self.__filter_action_keys:
+ filter_action = self.__filter_action_map[action_name]
+ obj = filter_action.to_json(queryset)
+ obj['action_name'] = action_name
+ filter_actions.append(obj)
+
+ return {
+ 'name': self.name,
+ 'title': self.title,
+ 'filter_actions': filter_actions
+ }
+
+class TableFilterQueryHelper(object):
+ def dateStringsToQ(self, field_name, date_from_str, date_to_str):
+ """
+ Convert the date strings from_date_str and to_date_str into a
+ set of args in the form
+
+ {'<field_name>__gte': <date from>, '<field_name>__lte': <date to>}
+
+ where date_from and date_to are Django-timezone-aware dates; then
+ convert that into a Django Q object
+
+ Returns the Q object based on those criteria
+ """
+
+ # one of the values required for the filter is missing, so set
+ # it to the one which was supplied
+ if date_from_str == '':
+ date_from_str = date_to_str
+ elif date_to_str == '':
+ date_to_str = date_from_str
+
+ date_from_naive = dateparse.parse_datetime(date_from_str + ' 00:00:00')
+ date_to_naive = dateparse.parse_datetime(date_to_str + ' 23:59:59')
+
+ tz = timezone.get_default_timezone()
+ date_from = timezone.make_aware(date_from_naive, tz)
+ date_to = timezone.make_aware(date_to_naive, tz)
+
+ args = {}
+ args[field_name + '__gte'] = date_from
+ args[field_name + '__lte'] = date_to
+
+ return Q(**args)
+
+class TableFilterAction(object):
+ """
+ A filter action which displays in the filter popup for a ToasterTable
+ and uses an associated QuerysetFilter to filter the queryset for that
+ ToasterTable
+ """
+
+ def __init__(self, name, title, criteria):
+ self.name = name
+ self.title = title
+ self.criteria = criteria
+
+ # set in subclasses
+ self.type = None
+
+ def set_filter_params(self, params):
+ """
+ params: (str) a string of extra parameters for the action;
+ the structure of this string depends on the type of action;
+ it's ignored for a toggle filter action, which is just on or off
+ """
+ pass
+
+ def filter(self, queryset):
+ if self.criteria:
+ return queryset.filter(self.criteria)
+ else:
+ return queryset
+
+ def to_json(self, queryset):
+ """ Dump as a JSON object """
+ return {
+ 'title': self.title,
+ 'type': self.type,
+ 'count': self.filter(queryset).count()
+ }
+
+class TableFilterActionToggle(TableFilterAction):
+ """
+ A single filter action which will populate one radio button of
+ a ToasterTable filter popup; this filter can either be on or off and
+ has no other parameters
+ """
+
+ def __init__(self, *args):
+ super(TableFilterActionToggle, self).__init__(*args)
+ self.type = 'toggle'
+
+class TableFilterActionDay(TableFilterAction):
+ """
+ A filter action which filters according to the named datetime field and a
+ string representing a day ("today" or "yesterday")
+ """
+
+ TODAY = 'today'
+ YESTERDAY = 'yesterday'
+
+ def __init__(self, name, title, field, day,
+ query_helper = TableFilterQueryHelper()):
+ """
+ field: (string) the datetime field to filter by
+ day: (string) "today" or "yesterday"
+ """
+ super(TableFilterActionDay, self).__init__(name, title, None)
+ self.type = 'day'
+ self.field = field
+ self.day = day
+ self.query_helper = query_helper
+
+ def filter(self, queryset):
+ """
+ Apply the day filtering before returning the queryset;
+ this is done here as the value of the filter criteria changes
+ depending on when the filtering is applied
+ """
+
+ now = timezone.now()
+
+ if self.day == self.YESTERDAY:
+ increment = timedelta(days=1)
+ wanted_date = now - increment
+ else:
+ wanted_date = now
+
+ wanted_date_str = wanted_date.strftime('%Y-%m-%d')
+
+ self.criteria = self.query_helper.dateStringsToQ(
+ self.field,
+ wanted_date_str,
+ wanted_date_str
+ )
+
+ return queryset.filter(self.criteria)
+
+class TableFilterActionDateRange(TableFilterAction):
+ """
+ A filter action which will filter the queryset by a date range.
+ The date range can be set via set_params()
+ """
+
+ def __init__(self, name, title, field,
+ query_helper = TableFilterQueryHelper()):
+ """
+ field: (string) the field to find the max/min range from in the queryset
+ """
+ super(TableFilterActionDateRange, self).__init__(
+ name,
+ title,
+ None
+ )
+
+ self.type = 'daterange'
+ self.field = field
+ self.query_helper = query_helper
+
+ def set_filter_params(self, params):
+ """
+ This filter depends on the user selecting some input, so it needs
+ to have its parameters set before its queryset is filtered
+
+ params: (str) a string of extra parameters for the filtering
+ in the format "2015-12-09,2015-12-11" (from,to); this is passed in the
+ querystring and used to set the criteria on the QuerysetFilter
+ associated with this action
+ """
+
+ # if params are invalid, return immediately, resetting criteria
+ # on the QuerysetFilter
+ try:
+ date_from_str, date_to_str = params.split(',')
+ except ValueError:
+ self.criteria = None
+ return
+
+ # one of the values required for the filter is missing, so set
+ # it to the one which was supplied
+ self.criteria = self.query_helper.dateStringsToQ(
+ self.field,
+ date_from_str,
+ date_to_str
+ )
+
+ def to_json(self, queryset):
+ """ Dump as a JSON object """
+ data = super(TableFilterActionDateRange, self).to_json(queryset)
+
+ # additional data about the date range covered by the queryset's
+ # records, retrieved from its <field> column
+ data['min'] = queryset.aggregate(Min(self.field))[self.field + '__min']
+ data['max'] = queryset.aggregate(Max(self.field))[self.field + '__max']
+
+ # a range filter has a count of None, as the number of records it
+ # will select depends on the date range entered and we don't know
+ # that ahead of time
+ data['count'] = None
+
+ return data
+
+class TableFilterMap(object):
+ """
+ Map from field names to TableFilter objects for those fields
+ """
+
+ def __init__(self):
+ self.__filters = {}
+
+ def add_filter(self, filter_name, table_filter):
+ """ table_filter is an instance of Filter """
+ self.__filters[filter_name] = table_filter
+
+ def get_filter(self, filter_name):
+ return self.__filters[filter_name]
+
+ def to_json(self, queryset):
+ data = {}
+
+ for filter_name, table_filter in self.__filters.iteritems():
+ data[filter_name] = table_filter.to_json()
+
+ return data
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/tables.py b/yocto-poky/bitbake/lib/toaster/toastergui/tables.py
index 9c9cda4e9..2cc2f4eb7 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/tables.py
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/tables.py
@@ -21,29 +21,27 @@
from toastergui.widgets import ToasterTable
from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
-from orm.models import CustomImageRecipe, Package
-from django.db.models import Q, Max
+from orm.models import CustomImageRecipe, Package, Target, Build, LogMessage, Task
+from orm.models import CustomImagePackage
+from django.db.models import Q, Max, Sum, Count, When, Case, Value, IntegerField
from django.conf.urls import url
-from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse, resolve
+from django.http import HttpResponse
from django.views.generic import TemplateView
+from toastergui.tablefilter import TableFilter
+from toastergui.tablefilter import TableFilterActionToggle
+from toastergui.tablefilter import TableFilterActionDateRange
+from toastergui.tablefilter import TableFilterActionDay
-class ProjectFiltersMixin(object):
- """Common mixin for recipe, machine in project filters"""
+class ProjectFilters(object):
+ @staticmethod
+ def in_project(project_layers):
+ return Q(layer_version__in=project_layers)
- def filter_in_project(self, count_only=False):
- query = self.queryset.filter(layer_version__in=self.project_layers)
- if count_only:
- return query.count()
-
- self.queryset = query
-
- def filter_not_in_project(self, count_only=False):
- query = self.queryset.exclude(layer_version__in=self.project_layers)
- if count_only:
- return query.count()
-
- self.queryset = query
+ @staticmethod
+ def not_in_project(project_layers):
+ return ~(ProjectFilters.in_project(project_layers))
class LayersTable(ToasterTable):
"""Table of layers in Toaster"""
@@ -61,33 +59,32 @@ class LayersTable(ToasterTable):
return context
-
def setup_filters(self, *args, **kwargs):
project = Project.objects.get(pk=kwargs['pid'])
self.project_layers = ProjectLayer.objects.filter(project=project)
+ in_current_project_filter = TableFilter(
+ "in_current_project",
+ "Filter by project layers"
+ )
- self.add_filter(title="Filter by project layers",
- name="in_current_project",
- filter_actions=[
- self.make_filter_action("in_project", "Layers added to this project", self.filter_in_project),
- self.make_filter_action("not_in_project", "Layers not added to this project", self.filter_not_in_project)
- ])
-
- def filter_in_project(self, count_only=False):
- query = self.queryset.filter(projectlayer__in=self.project_layers)
- if count_only:
- return query.count()
+ criteria = Q(projectlayer__in=self.project_layers)
- self.queryset = query
+ in_project_action = TableFilterActionToggle(
+ "in_project",
+ "Layers added to this project",
+ criteria
+ )
- def filter_not_in_project(self, count_only=False):
- query = self.queryset.exclude(projectlayer__in=self.project_layers)
- if count_only:
- return query.count()
-
- self.queryset = query
+ not_in_project_action = TableFilterActionToggle(
+ "not_in_project",
+ "Layers not added to this project",
+ ~criteria
+ )
+ in_current_project_filter.add_action(in_project_action)
+ in_current_project_filter.add_action(not_in_project_action)
+ self.add_filter(in_current_project_filter)
def setup_queryset(self, *args, **kwargs):
prj = Project.objects.get(pk = kwargs['pid'])
@@ -161,7 +158,7 @@ class LayersTable(ToasterTable):
{% endwith %}
'''
- self.add_column(title="Revision",
+ self.add_column(title="Git revision",
help_text="The Git branch, tag or commit. For the layers from the OpenEmbedded layer source, the revision is always the branch compatible with the Yocto Project version you selected for this project",
static_data_name="revision",
static_data_template=revision_template)
@@ -186,8 +183,8 @@ class LayersTable(ToasterTable):
static_data_name="dependencies",
static_data_template=deps_template)
- self.add_column(title="Add | Delete",
- help_text="Add or delete layers to / from your project",
+ self.add_column(title="Add | Remove",
+ help_text="Add or remove layers to / from your project",
hideable=False,
filter_name="in_current_project",
static_data_name="add-del-layers",
@@ -205,7 +202,7 @@ class LayersTable(ToasterTable):
computation = lambda x: x.layer.name)
-class MachinesTable(ToasterTable, ProjectFiltersMixin):
+class MachinesTable(ToasterTable):
"""Table of Machines in Toaster"""
def __init__(self, *args, **kwargs):
@@ -217,25 +214,41 @@ class MachinesTable(ToasterTable, ProjectFiltersMixin):
def get_context_data(self, **kwargs):
context = super(MachinesTable, self).get_context_data(**kwargs)
context['project'] = Project.objects.get(pk=kwargs['pid'])
- context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project']))
return context
def setup_filters(self, *args, **kwargs):
project = Project.objects.get(pk=kwargs['pid'])
- self.project_layers = project.get_project_layer_versions()
- self.add_filter(title="Filter by project machines",
- name="in_current_project",
- filter_actions=[
- self.make_filter_action("in_project", "Machines provided by layers added to this project", self.filter_in_project),
- self.make_filter_action("not_in_project", "Machines provided by layers not added to this project", self.filter_not_in_project)
- ])
+ in_current_project_filter = TableFilter(
+ "in_current_project",
+ "Filter by project machines"
+ )
+
+ in_project_action = TableFilterActionToggle(
+ "in_project",
+ "Machines provided by layers added to this project",
+ ProjectFilters.in_project(self.project_layers)
+ )
+
+ not_in_project_action = TableFilterActionToggle(
+ "not_in_project",
+ "Machines provided by layers not added to this project",
+ ProjectFilters.not_in_project(self.project_layers)
+ )
+
+ in_current_project_filter.add_action(in_project_action)
+ in_current_project_filter.add_action(not_in_project_action)
+ self.add_filter(in_current_project_filter)
def setup_queryset(self, *args, **kwargs):
prj = Project.objects.get(pk = kwargs['pid'])
self.queryset = prj.get_all_compatible_machines()
self.queryset = self.queryset.order_by(self.default_orderby)
+ self.static_context_extra['current_layers'] = \
+ self.project_layers = \
+ prj.get_project_layer_versions(pk=True)
+
def setup_columns(self, *args, **kwargs):
self.add_column(title="Machine",
@@ -256,7 +269,7 @@ class MachinesTable(ToasterTable, ProjectFiltersMixin):
static_data_template=layer_link_template,
orderable=True)
- self.add_column(title="Revision",
+ self.add_column(title="Git revision",
help_text="The Git branch, tag or commit. For the layers from the OpenEmbedded layer source, the revision is always the branch compatible with the Yocto Project version you selected for this project",
hidden=True,
field_name="layer_version__get_vcs_reference")
@@ -293,6 +306,7 @@ class LayerMachinesTable(MachinesTable):
MachinesTable.setup_queryset(self, *args, **kwargs)
self.queryset = self.queryset.filter(layer_version__pk=int(kwargs['layerid']))
+ self.queryset = self.queryset.order_by(self.default_orderby)
self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
def setup_columns(self, *args, **kwargs):
@@ -311,13 +325,12 @@ class LayerMachinesTable(MachinesTable):
static_data_template=select_btn_template)
-class RecipesTable(ToasterTable, ProjectFiltersMixin):
+class RecipesTable(ToasterTable):
"""Table of All Recipes in Toaster"""
def __init__(self, *args, **kwargs):
super(RecipesTable, self).__init__(*args, **kwargs)
self.empty_state = "Toaster has no recipe information. To generate recipe information you can configure a layer source then run a build."
- self.default_orderby = "name"
build_col = { 'title' : "Build",
'help_text' : "Add or delete recipes to and from your project",
@@ -337,12 +350,26 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin):
return context
def setup_filters(self, *args, **kwargs):
- self.add_filter(title="Filter by project recipes",
- name="in_current_project",
- filter_actions=[
- self.make_filter_action("in_project", "Recipes provided by layers added to this project", self.filter_in_project),
- self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", self.filter_not_in_project)
- ])
+ table_filter = TableFilter(
+ 'in_current_project',
+ 'Filter by project recipes'
+ )
+
+ in_project_action = TableFilterActionToggle(
+ 'in_project',
+ 'Recipes provided by layers added to this project',
+ ProjectFilters.in_project(self.project_layers)
+ )
+
+ not_in_project_action = TableFilterActionToggle(
+ 'not_in_project',
+ 'Recipes provided by layers not added to this project',
+ ProjectFilters.not_in_project(self.project_layers)
+ )
+
+ table_filter.add_action(in_project_action)
+ table_filter.add_action(not_in_project_action)
+ self.add_filter(table_filter)
def setup_queryset(self, *args, **kwargs):
prj = Project.objects.get(pk = kwargs['pid'])
@@ -354,7 +381,6 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin):
self.static_context_extra['current_layers'] = self.project_layers
self.queryset = prj.get_all_compatible_recipes()
- self.queryset = self.queryset.order_by(self.default_orderby)
def setup_columns(self, *args, **kwargs):
@@ -402,7 +428,7 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin):
orderable=True,
field_name="license")
- self.add_column(title="Revision",
+ self.add_column(title="Git revision",
hidden=True,
field_name="layer_version__get_vcs_reference")
@@ -412,6 +438,7 @@ class LayerRecipesTable(RecipesTable):
def __init__(self, *args, **kwargs):
super(LayerRecipesTable, self).__init__(*args, **kwargs)
+ self.default_orderby = "name"
def get_context_data(self, **kwargs):
context = super(LayerRecipesTable, self).get_context_data(**kwargs)
@@ -423,6 +450,7 @@ class LayerRecipesTable(RecipesTable):
self.queryset = \
Recipe.objects.filter(layer_version__pk=int(kwargs['layerid']))
+ self.queryset = self.queryset.order_by(self.default_orderby)
self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
def setup_columns(self, *args, **kwargs):
@@ -449,18 +477,19 @@ class CustomImagesTable(ToasterTable):
def __init__(self, *args, **kwargs):
super(CustomImagesTable, self).__init__(*args, **kwargs)
self.title = "Custom images"
+ self.default_orderby = "name"
def get_context_data(self, **kwargs):
context = super(CustomImagesTable, self).get_context_data(**kwargs)
project = Project.objects.get(pk=kwargs['pid'])
+ # TODO put project into the ToasterTable base class
context['project'] = project
- context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project']))
return context
def setup_queryset(self, *args, **kwargs):
prj = Project.objects.get(pk = kwargs['pid'])
self.queryset = CustomImageRecipe.objects.filter(project=prj)
- self.queryset = self.queryset.order_by('name')
+ self.queryset = self.queryset.order_by(self.default_orderby)
def setup_columns(self, *args, **kwargs):
@@ -472,22 +501,37 @@ class CustomImagesTable(ToasterTable):
self.add_column(title="Custom image",
hideable=False,
+ orderable=True,
+ field_name="name",
static_data_name="name",
static_data_template=name_link_template)
+ recipe_file_template = '''
+ <code>{{data.name}}_{{data.version}}.bb</code>
+ <a href="{% url 'customrecipedownload' extra.pid data.pk %}">
+ <i class="icon-download-alt" data-original-title="Download recipe
+ file"></i>
+ </a>'''
+
self.add_column(title="Recipe file",
- static_data_name='recipe_file',
- static_data_template='')
+ static_data_name='recipe_file_download',
+ static_data_template=recipe_file_template)
+
+ approx_packages_template = '''
+ <a href="{% url 'customrecipe' extra.pid data.id %}">
+ {{data.get_all_packages.count}}
+ </a>'''
- approx_packages_template = '<a href="#imagedetails">{{data.packages.all|length}}</a>'
self.add_column(title="Approx packages",
static_data_name='approx_packages',
static_data_template=approx_packages_template)
- build_btn_template = '''<button data-recipe-name="{{data.name}}"
+ build_btn_template = '''
+ <button data-recipe-name="{{data.name}}"
class="btn btn-block build-recipe-btn" style="margin-top: 5px;" >
- Build</button>'''
+ Build
+ </button>'''
self.add_column(title="Build",
hideable=False,
@@ -500,20 +544,32 @@ class ImageRecipesTable(RecipesTable):
def __init__(self, *args, **kwargs):
super(ImageRecipesTable, self).__init__(*args, **kwargs)
self.title = "Compatible image recipes"
+ self.default_orderby = "name"
def setup_queryset(self, *args, **kwargs):
super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
- self.queryset = self.queryset.filter(is_image=True)
+ custom_image_recipes = CustomImageRecipe.objects.filter(
+ project=kwargs['pid'])
+ self.queryset = self.queryset.filter(
+ Q(is_image=True) & ~Q(pk__in=custom_image_recipes))
+ self.queryset = self.queryset.order_by(self.default_orderby)
def setup_columns(self, *args, **kwargs):
+
+ name_link_template = '''
+ <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
+ '''
+
self.add_column(title="Image recipe",
help_text="When you build an image recipe, you get an "
"image: a root file system you can"
"deploy to a machine",
hideable=False,
orderable=True,
+ static_data_name="name",
+ static_data_template=name_link_template,
field_name="name")
super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
@@ -529,17 +585,27 @@ class NewCustomImagesTable(ImageRecipesTable):
def setup_queryset(self, *args, **kwargs):
super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
+ prj = Project.objects.get(pk = kwargs['pid'])
+ self.static_context_extra['current_layers'] = \
+ prj.get_project_layer_versions(pk=True)
self.queryset = self.queryset.filter(is_image=True)
def setup_columns(self, *args, **kwargs):
+
+ name_link_template = '''
+ <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
+ '''
+
self.add_column(title="Image recipe",
help_text="When you build an image recipe, you get an "
"image: a root file system you can"
"deploy to a machine",
hideable=False,
orderable=True,
- field_name="recipe__name")
+ static_data_name="name",
+ static_data_template=name_link_template,
+ field_name="name")
super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
@@ -555,11 +621,13 @@ class SoftwareRecipesTable(RecipesTable):
def __init__(self, *args, **kwargs):
super(SoftwareRecipesTable, self).__init__(*args, **kwargs)
self.title = "Compatible software recipes"
+ self.default_orderby = "name"
def setup_queryset(self, *args, **kwargs):
super(SoftwareRecipesTable, self).setup_queryset(*args, **kwargs)
self.queryset = self.queryset.filter(is_image=False)
+ self.queryset = self.queryset.order_by(self.default_orderby)
def setup_columns(self, *args, **kwargs):
@@ -576,33 +644,60 @@ class SoftwareRecipesTable(RecipesTable):
self.add_column(**RecipesTable.build_col)
-
-class SelectPackagesTable(ToasterTable):
- """ Table to display the packages to add and remove from an image """
+class PackagesTable(ToasterTable):
+ """ Table to display the packages in a recipe from it's last successful
+ build"""
def __init__(self, *args, **kwargs):
- super(SelectPackagesTable, self).__init__(*args, **kwargs)
- self.title = "Add | Remove packages"
+ super(PackagesTable, self).__init__(*args, **kwargs)
+ self.title = "Packages included"
+ self.packages = None
+ self.default_orderby = "name"
- def setup_queryset(self, *args, **kwargs):
- cust_recipe = CustomImageRecipe.objects.get(pk=kwargs['recipeid'])
- prj = Project.objects.get(pk = kwargs['pid'])
+ def create_package_list(self, recipe, project_id):
+ """Creates a list of packages for the specified recipe by looking for
+ the last SUCCEEDED build of ther recipe"""
- current_packages = cust_recipe.packages.all()
+ target = Target.objects.filter(Q(target=recipe.name) &
+ Q(build__project_id=project_id) &
+ Q(build__outcome=Build.SUCCEEDED)
+ ).last()
- # Get all the packages that are in the custom image
- # Get all the packages built by builds in the current project
- # but not those ones that are already in the custom image
- self.queryset = Package.objects.filter(
- Q(pk__in=current_packages) |
- (Q(build__project=prj) &
- ~Q(name__in=current_packages.values_list('name'))))
+ if target:
+ pkgs = target.target_installed_package_set.values_list('package',
+ flat=True)
+ return Package.objects.filter(pk__in=pkgs)
- self.queryset = self.queryset.order_by('name')
+ # Target/recipe never successfully built so empty queryset
+ return Package.objects.none()
- self.static_context_extra['recipe_id'] = kwargs['recipeid']
- self.static_context_extra['current_packages'] = \
- cust_recipe.packages.values_list('pk', flat=True)
+ def get_context_data(self, **kwargs):
+ """Context for rendering the sidebar and other items on the recipe
+ details page """
+ context = super(PackagesTable, self).get_context_data(**kwargs)
+
+ recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
+ project = Project.objects.get(pk=kwargs['pid'])
+
+ in_project = (recipe.layer_version.pk in
+ project.get_project_layer_versions(pk=True))
+
+ packages = self.create_package_list(recipe, project.pk)
+
+ context.update({'project': project,
+ 'recipe' : recipe,
+ 'packages': packages,
+ 'approx_pkg_size' : packages.aggregate(Sum('size')),
+ 'in_project' : in_project,
+ })
+
+ return context
+
+ def setup_queryset(self, *args, **kwargs):
+ recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
+
+ self.queryset = self.create_package_list(recipe, kwargs['pid'])
+ self.queryset = self.queryset.order_by('name')
def setup_columns(self, *args, **kwargs):
self.add_column(title="Package",
@@ -611,18 +706,798 @@ class SelectPackagesTable(ToasterTable):
field_name="name")
self.add_column(title="Package Version",
- field_name="version")
+ field_name="version",
+ hideable=False)
self.add_column(title="Approx Size",
orderable=True,
static_data_name="size",
static_data_template="{% load projecttags %} \
{{data.size|filtered_filesizeformat}}")
- self.add_column(title="summary",
- field_name="summary")
+
+ self.add_column(title="License",
+ field_name="license",
+ orderable=True)
+
+
+ self.add_column(title="Dependencies",
+ static_data_name="dependencies",
+ static_data_template='\
+ {% include "snippets/pkg_dependencies_popover.html" %}')
+
+ self.add_column(title="Reverse dependencies",
+ static_data_name="reverse_dependencies",
+ static_data_template='\
+ {% include "snippets/pkg_revdependencies_popover.html" %}',
+ hidden=True)
+
+ self.add_column(title="Recipe",
+ field_name="recipe__name",
+ orderable=True,
+ hidden=True)
+
+ self.add_column(title="Recipe version",
+ field_name="recipe__version",
+ hidden=True)
+
+
+class SelectPackagesTable(PackagesTable):
+ """ Table to display the packages to add and remove from an image """
+
+ def __init__(self, *args, **kwargs):
+ super(SelectPackagesTable, self).__init__(*args, **kwargs)
+ self.title = "Add | Remove packages"
+
+ def setup_queryset(self, *args, **kwargs):
+ self.cust_recipe =\
+ CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
+ prj = Project.objects.get(pk = kwargs['pid'])
+
+ current_packages = self.cust_recipe.get_all_packages()
+
+ current_recipes = prj.get_available_recipes()
+
+ # only show packages where recipes->layers are in the project
+ self.queryset = CustomImagePackage.objects.filter(
+ ~Q(recipe=None) &
+ Q(recipe__in=current_recipes))
+
+ self.queryset = self.queryset.order_by('name')
+
+ self.static_context_extra['recipe_id'] = kwargs['custrecipeid']
+ self.static_context_extra['current_packages'] = \
+ current_packages.values_list('pk', flat=True)
+
+ def get_context_data(self, **kwargs):
+ # to reuse the Super class map the custrecipeid to the recipe_id
+ kwargs['recipe_id'] = kwargs['custrecipeid']
+ context = super(SelectPackagesTable, self).get_context_data(**kwargs)
+ custom_recipe = \
+ CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
+
+ context['recipe'] = custom_recipe
+ context['approx_pkg_size'] = \
+ custom_recipe.get_all_packages().aggregate(Sum('size'))
+ return context
+
+
+ def setup_columns(self, *args, **kwargs):
+ super(SelectPackagesTable, self).setup_columns(*args, **kwargs)
+
+ add_remove_template = '{% include "pkg_add_rm_btn.html" %}'
self.add_column(title="Add | Remove",
+ hideable=False,
help_text="Use the add and remove buttons to modify "
- "the package content of you custom image",
+ "the package content of your custom image",
static_data_name="add_rm_pkg_btn",
- static_data_template='{% include "pkg_add_rm_btn.html" %}')
+ static_data_template=add_remove_template,
+ filter_name='in_current_image_filter')
+
+ def setup_filters(self, *args, **kwargs):
+ in_current_image_filter = TableFilter(
+ 'in_current_image_filter',
+ 'Filter by added packages'
+ )
+
+ in_image_action = TableFilterActionToggle(
+ 'in_image',
+ 'Packages in %s' % self.cust_recipe.name,
+ Q(pk__in=self.static_context_extra['current_packages'])
+ )
+
+ not_in_image_action = TableFilterActionToggle(
+ 'not_in_image',
+ 'Packages not added to %s' % self.cust_recipe.name,
+ ~Q(pk__in=self.static_context_extra['current_packages'])
+ )
+
+ in_current_image_filter.add_action(in_image_action)
+ in_current_image_filter.add_action(not_in_image_action)
+ self.add_filter(in_current_image_filter)
+
+class ProjectsTable(ToasterTable):
+ """Table of projects in Toaster"""
+
+ def __init__(self, *args, **kwargs):
+ super(ProjectsTable, self).__init__(*args, **kwargs)
+ self.default_orderby = '-updated'
+ self.title = 'All projects'
+ self.static_context_extra['Build'] = Build
+
+ def get_context_data(self, **kwargs):
+ return super(ProjectsTable, self).get_context_data(**kwargs)
+
+ def setup_queryset(self, *args, **kwargs):
+ queryset = Project.objects.all()
+
+ # annotate each project with its number of builds
+ queryset = queryset.annotate(num_builds=Count('build'))
+
+ # exclude the command line builds project if it has no builds
+ q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0)
+ queryset = queryset.filter(Q(is_default=False) |
+ q_default_with_builds)
+
+ # order rows
+ queryset = queryset.order_by(self.default_orderby)
+
+ self.queryset = queryset
+
+ # columns: last activity on (updated) - DEFAULT, project (name), release,
+ # machine, number of builds, last build outcome, recipe (name), errors,
+ # warnings, image files
+ def setup_columns(self, *args, **kwargs):
+ name_template = '''
+ {% load project_url_tag %}
+ <span data-project-field="name">
+ <a href="{% project_url data %}">
+ {{data.name}}
+ </a>
+ </span>
+ '''
+
+ last_activity_on_template = '''
+ {% load project_url_tag %}
+ <span data-project-field="updated">
+ <a href="{% project_url data %}">
+ {{data.updated | date:"d/m/y H:i"}}
+ </a>
+ </span>
+ '''
+
+ release_template = '''
+ <span data-project-field="release">
+ {% if data.release %}
+ <a href="{% url 'project' data.id %}#project-details">
+ {{data.release.name}}
+ </a>
+ {% elif data.is_default %}
+ <span class="muted">Not applicable</span>
+ <i class="icon-question-sign get-help hover-help"
+ data-original-title="This project does not have a release set.
+ It simply collects information about the builds you start from
+ the command line while Toaster is running"
+ style="visibility: hidden;">
+ </i>
+ {% else %}
+ No release available
+ {% endif %}
+ </span>
+ '''
+
+ machine_template = '''
+ <span data-project-field="machine">
+ {% if data.is_default %}
+ <span class="muted">Not applicable</span>
+ <i class="icon-question-sign get-help hover-help"
+ data-original-title="This project does not have a machine
+ set. It simply collects information about the builds you
+ start from the command line while Toaster is running"
+ style="visibility: hidden;"></i>
+ {% else %}
+ <a href="{% url 'project' data.id %}#machine-distro">
+ {{data.get_current_machine_name}}
+ </a>
+ {% endif %}
+ </span>
+ '''
+
+ number_of_builds_template = '''
+ {% if data.get_number_of_builds > 0 %}
+ <a href="{% url 'projectbuilds' data.id %}">
+ {{data.get_number_of_builds}}
+ </a>
+ {% else %}
+ <span class="muted">0</span>
+ {% endif %}
+ '''
+
+ last_build_outcome_template = '''
+ {% if data.get_number_of_builds > 0 %}
+ <a href="{% url 'builddashboard' data.get_last_build_id %}">
+ {% if data.get_last_outcome == extra.Build.SUCCEEDED %}
+ <i class="icon-ok-sign success"></i>
+ {% elif data.get_last_outcome == extra.Build.FAILED %}
+ <i class="icon-minus-sign error"></i>
+ {% endif %}
+ </a>
+ {% endif %}
+ '''
+
+ recipe_template = '''
+ {% if data.get_number_of_builds > 0 %}
+ <a href="{% url "builddashboard" data.get_last_build_id %}">
+ {{data.get_last_target}}
+ </a>
+ {% endif %}
+ '''
+
+ errors_template = '''
+ {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %}
+ <a class="errors.count error"
+ href="{% url "builddashboard" data.get_last_build_id %}#errors">
+ {{data.get_last_errors}} error{{data.get_last_errors | pluralize}}
+ </a>
+ {% endif %}
+ '''
+
+ warnings_template = '''
+ {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %}
+ <a class="warnings.count warning"
+ href="{% url "builddashboard" data.get_last_build_id %}#warnings">
+ {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}}
+ </a>
+ {% endif %}
+ '''
+
+ image_files_template = '''
+ {% if data.get_number_of_builds > 0 and data.get_last_outcome == extra.Build.SUCCEEDED %}
+ <a href="{% url "builddashboard" data.get_last_build_id %}#images">
+ {{data.get_last_build_extensions}}
+ </a>
+ {% endif %}
+ '''
+
+ self.add_column(title='Project',
+ hideable=False,
+ orderable=True,
+ static_data_name='name',
+ static_data_template=name_template)
+
+ self.add_column(title='Last activity on',
+ help_text='Starting date and time of the \
+ last project build. If the project has no \
+ builds, this shows the date the project was \
+ created.',
+ hideable=False,
+ orderable=True,
+ static_data_name='updated',
+ static_data_template=last_activity_on_template)
+
+ self.add_column(title='Release',
+ help_text='The version of the build system used by \
+ the project',
+ hideable=False,
+ orderable=True,
+ static_data_name='release',
+ static_data_template=release_template)
+
+ self.add_column(title='Machine',
+ help_text='The hardware currently selected for the \
+ project',
+ hideable=False,
+ orderable=False,
+ static_data_name='machine',
+ static_data_template=machine_template)
+
+ self.add_column(title='Number of builds',
+ help_text='The number of builds which have been run \
+ for the project',
+ hideable=False,
+ orderable=False,
+ static_data_name='number_of_builds',
+ static_data_template=number_of_builds_template)
+
+ self.add_column(title='Last build outcome',
+ help_text='Indicates whether the last project build \
+ completed successfully or failed',
+ hideable=True,
+ orderable=False,
+ static_data_name='last_build_outcome',
+ static_data_template=last_build_outcome_template)
+
+ self.add_column(title='Recipe',
+ help_text='The last recipe which was built in this \
+ project',
+ hideable=True,
+ orderable=False,
+ static_data_name='recipe_name',
+ static_data_template=recipe_template)
+
+ self.add_column(title='Errors',
+ help_text='The number of errors encountered during \
+ the last project build (if any)',
+ hideable=True,
+ orderable=False,
+ static_data_name='errors',
+ static_data_template=errors_template)
+
+ self.add_column(title='Warnings',
+ help_text='The number of warnings encountered during \
+ the last project build (if any)',
+ hideable=True,
+ hidden=True,
+ orderable=False,
+ static_data_name='warnings',
+ static_data_template=warnings_template)
+
+ self.add_column(title='Image files',
+ help_text='The root file system types produced by \
+ the last project build',
+ hideable=True,
+ hidden=True,
+ orderable=False,
+ static_data_name='image_files',
+ static_data_template=image_files_template)
+
+class BuildsTable(ToasterTable):
+ """Table of builds in Toaster"""
+
+ def __init__(self, *args, **kwargs):
+ super(BuildsTable, self).__init__(*args, **kwargs)
+ self.default_orderby = '-completed_on'
+ self.static_context_extra['Build'] = Build
+ self.static_context_extra['Task'] = Task
+
+ # attributes that are overridden in subclasses
+
+ # title for the page
+ self.title = ''
+
+ # 'project' or 'all'; determines how the mrb (most recent builds)
+ # section is displayed
+ self.mrb_type = ''
+
+ def get_builds(self):
+ """
+ overridden in ProjectBuildsTable to return builds for a
+ single project
+ """
+ return Build.objects.all()
+
+ def get_context_data(self, **kwargs):
+ context = super(BuildsTable, self).get_context_data(**kwargs)
+
+ # should be set in subclasses
+ context['mru'] = []
+
+ context['mrb_type'] = self.mrb_type
+
+ return context
+
+ def setup_queryset(self, *args, **kwargs):
+ """
+ The queryset is annotated so that it can be sorted by number of
+ errors and number of warnings; but note that the criteria for
+ finding the log messages to populate these fields should match those
+ used in the Build model (orm/models.py) to populate the errors and
+ warnings properties
+ """
+ queryset = self.get_builds()
+
+ # Don't include in progress builds pr cancelled builds
+ queryset = queryset.exclude(Q(outcome=Build.IN_PROGRESS) |
+ Q(outcome=Build.CANCELLED))
+
+ # sort
+ queryset = queryset.order_by(self.default_orderby)
+
+ # annotate with number of ERROR, EXCEPTION and CRITICAL log messages
+ criteria = (Q(logmessage__level=LogMessage.ERROR) |
+ Q(logmessage__level=LogMessage.EXCEPTION) |
+ Q(logmessage__level=LogMessage.CRITICAL))
+
+ queryset = queryset.annotate(
+ errors_no=Count(
+ Case(
+ When(criteria, then=Value(1)),
+ output_field=IntegerField()
+ )
+ )
+ )
+
+ # annotate with number of WARNING log messages
+ queryset = queryset.annotate(
+ warnings_no=Count(
+ Case(
+ When(logmessage__level=LogMessage.WARNING, then=Value(1)),
+ output_field=IntegerField()
+ )
+ )
+ )
+
+ self.queryset = queryset
+
+ def setup_columns(self, *args, **kwargs):
+ outcome_template = '''
+ <a href="{% url "builddashboard" data.id %}">
+ {% if data.outcome == data.SUCCEEDED %}
+ <i class="icon-ok-sign success"></i>
+ {% elif data.outcome == data.FAILED %}
+ <i class="icon-minus-sign error"></i>
+ {% endif %}
+ </a>
+
+ {% if data.cooker_log_path %}
+ &nbsp;
+ <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
+ <i class="icon-download-alt get-help"
+ data-original-title="Download build log"></i>
+ </a>
+ {% endif %}
+ '''
+
+ recipe_template = '''
+ {% for target_label in data.target_labels %}
+ <a href="{% url "builddashboard" data.id %}">
+ {{target_label}}
+ </a>
+ <br />
+ {% endfor %}
+ '''
+
+ machine_template = '''
+ <a href="{% url "builddashboard" data.id %}">
+ {{data.machine}}
+ </a>
+ '''
+
+ started_on_template = '''
+ <a href="{% url "builddashboard" data.id %}">
+ {{data.started_on | date:"d/m/y H:i"}}
+ </a>
+ '''
+
+ completed_on_template = '''
+ <a href="{% url "builddashboard" data.id %}">
+ {{data.completed_on | date:"d/m/y H:i"}}
+ </a>
+ '''
+
+ failed_tasks_template = '''
+ {% if data.failed_tasks.count == 1 %}
+ <a href="{% url "task" data.id data.failed_tasks.0.id %}">
+ <span class="error">
+ {{data.failed_tasks.0.recipe.name}}.{{data.failed_tasks.0.task_name}}
+ </span>
+ </a>
+ <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}">
+ <i class="icon-download-alt"
+ data-original-title="Download task log file">
+ </i>
+ </a>
+ {% elif data.failed_tasks.count > 1 %}
+ <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}">
+ <span class="error">{{data.failed_tasks.count}} tasks</span>
+ </a>
+ {% endif %}
+ '''
+
+ errors_template = '''
+ {% if data.errors_no %}
+ <a class="errors.count error" href="{% url "builddashboard" data.id %}#errors">
+ {{data.errors_no}} error{{data.errors_no|pluralize}}
+ </a>
+ {% endif %}
+ '''
+
+ warnings_template = '''
+ {% if data.warnings_no %}
+ <a class="warnings.count warning" href="{% url "builddashboard" data.id %}#warnings">
+ {{data.warnings_no}} warning{{data.warnings_no|pluralize}}
+ </a>
+ {% endif %}
+ '''
+
+ time_template = '''
+ {% load projecttags %}
+ <a href="{% url "buildtime" data.id %}">
+ {{data.timespent_seconds | sectohms}}
+ </a>
+ '''
+
+ image_files_template = '''
+ {% if data.outcome == extra.Build.SUCCEEDED %}
+ <a href="{% url "builddashboard" data.id %}#images">
+ {{data.get_image_file_extensions}}
+ </a>
+ {% endif %}
+ '''
+
+ self.add_column(title='Outcome',
+ help_text='Final state of the build (successful \
+ or failed)',
+ hideable=False,
+ orderable=True,
+ filter_name='outcome_filter',
+ static_data_name='outcome',
+ static_data_template=outcome_template)
+
+ self.add_column(title='Recipe',
+ help_text='What was built (i.e. one or more recipes \
+ or image recipes)',
+ hideable=False,
+ orderable=False,
+ static_data_name='target',
+ static_data_template=recipe_template)
+
+ self.add_column(title='Machine',
+ help_text='Hardware for which you are building a \
+ recipe or image recipe',
+ hideable=False,
+ orderable=True,
+ static_data_name='machine',
+ static_data_template=machine_template)
+
+ self.add_column(title='Started on',
+ help_text='The date and time when the build started',
+ hideable=True,
+ hidden=True,
+ orderable=True,
+ filter_name='started_on_filter',
+ static_data_name='started_on',
+ static_data_template=started_on_template)
+
+ self.add_column(title='Completed on',
+ help_text='The date and time when the build finished',
+ hideable=False,
+ orderable=True,
+ filter_name='completed_on_filter',
+ static_data_name='completed_on',
+ static_data_template=completed_on_template)
+
+ self.add_column(title='Failed tasks',
+ help_text='The number of tasks which failed during \
+ the build',
+ hideable=True,
+ orderable=False,
+ filter_name='failed_tasks_filter',
+ static_data_name='failed_tasks',
+ static_data_template=failed_tasks_template)
+
+ self.add_column(title='Errors',
+ help_text='The number of errors encountered during \
+ the build (if any)',
+ hideable=True,
+ orderable=True,
+ static_data_name='errors_no',
+ static_data_template=errors_template)
+
+ self.add_column(title='Warnings',
+ help_text='The number of warnings encountered during \
+ the build (if any)',
+ hideable=True,
+ orderable=True,
+ static_data_name='warnings_no',
+ static_data_template=warnings_template)
+
+ self.add_column(title='Time',
+ help_text='How long the build took to finish',
+ hideable=True,
+ hidden=True,
+ orderable=False,
+ static_data_name='time',
+ static_data_template=time_template)
+
+ self.add_column(title='Image files',
+ help_text='The root file system types produced by \
+ the build',
+ hideable=True,
+ orderable=False,
+ static_data_name='image_files',
+ static_data_template=image_files_template)
+
+ def setup_filters(self, *args, **kwargs):
+ # outcomes
+ outcome_filter = TableFilter(
+ 'outcome_filter',
+ 'Filter builds by outcome'
+ )
+
+ successful_builds_action = TableFilterActionToggle(
+ 'successful_builds',
+ 'Successful builds',
+ Q(outcome=Build.SUCCEEDED)
+ )
+
+ failed_builds_action = TableFilterActionToggle(
+ 'failed_builds',
+ 'Failed builds',
+ Q(outcome=Build.FAILED)
+ )
+
+ outcome_filter.add_action(successful_builds_action)
+ outcome_filter.add_action(failed_builds_action)
+ self.add_filter(outcome_filter)
+
+ # started on
+ started_on_filter = TableFilter(
+ 'started_on_filter',
+ 'Filter by date when build was started'
+ )
+
+ started_today_action = TableFilterActionDay(
+ 'today',
+ 'Today\'s builds',
+ 'started_on',
+ 'today'
+ )
+
+ started_yesterday_action = TableFilterActionDay(
+ 'yesterday',
+ 'Yesterday\'s builds',
+ 'started_on',
+ 'yesterday'
+ )
+
+ by_started_date_range_action = TableFilterActionDateRange(
+ 'date_range',
+ 'Build date range',
+ 'started_on'
+ )
+
+ started_on_filter.add_action(started_today_action)
+ started_on_filter.add_action(started_yesterday_action)
+ started_on_filter.add_action(by_started_date_range_action)
+ self.add_filter(started_on_filter)
+
+ # completed on
+ completed_on_filter = TableFilter(
+ 'completed_on_filter',
+ 'Filter by date when build was completed'
+ )
+
+ completed_today_action = TableFilterActionDay(
+ 'today',
+ 'Today\'s builds',
+ 'completed_on',
+ 'today'
+ )
+
+ completed_yesterday_action = TableFilterActionDay(
+ 'yesterday',
+ 'Yesterday\'s builds',
+ 'completed_on',
+ 'yesterday'
+ )
+
+ by_completed_date_range_action = TableFilterActionDateRange(
+ 'date_range',
+ 'Build date range',
+ 'completed_on'
+ )
+
+ completed_on_filter.add_action(completed_today_action)
+ completed_on_filter.add_action(completed_yesterday_action)
+ completed_on_filter.add_action(by_completed_date_range_action)
+ self.add_filter(completed_on_filter)
+
+ # failed tasks
+ failed_tasks_filter = TableFilter(
+ 'failed_tasks_filter',
+ 'Filter builds by failed tasks'
+ )
+
+ criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
+
+ with_failed_tasks_action = TableFilterActionToggle(
+ 'with_failed_tasks',
+ 'Builds with failed tasks',
+ criteria
+ )
+
+ without_failed_tasks_action = TableFilterActionToggle(
+ 'without_failed_tasks',
+ 'Builds without failed tasks',
+ ~criteria
+ )
+
+ failed_tasks_filter.add_action(with_failed_tasks_action)
+ failed_tasks_filter.add_action(without_failed_tasks_action)
+ self.add_filter(failed_tasks_filter)
+
+
+class AllBuildsTable(BuildsTable):
+ """ Builds page for all builds """
+
+ def __init__(self, *args, **kwargs):
+ super(AllBuildsTable, self).__init__(*args, **kwargs)
+ self.title = 'All builds'
+ self.mrb_type = 'all'
+
+ def setup_columns(self, *args, **kwargs):
+ """
+ All builds page shows a column for the project
+ """
+
+ super(AllBuildsTable, self).setup_columns(*args, **kwargs)
+
+ project_template = '''
+ {% load project_url_tag %}
+ <a href="{% project_url data.project %}">
+ {{data.project.name}}
+ </a>
+ {% if data.project.is_default %}
+ <i class="icon-question-sign get-help hover-help" title=""
+ data-original-title="This project shows information about
+ the builds you start from the command line while Toaster is
+ running" style="visibility: hidden;"></i>
+ {% endif %}
+ '''
+
+ self.add_column(title='Project',
+ hideable=True,
+ orderable=True,
+ static_data_name='project',
+ static_data_template=project_template)
+
+ def get_context_data(self, **kwargs):
+ """ Get all builds for the recent builds area """
+ context = super(AllBuildsTable, self).get_context_data(**kwargs)
+ context['mru'] = Build.get_recent()
+ return context
+
+class ProjectBuildsTable(BuildsTable):
+ """
+ Builds page for a single project; a BuildsTable, with the queryset
+ filtered by project
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(ProjectBuildsTable, self).__init__(*args, **kwargs)
+ self.title = 'All project builds'
+ self.mrb_type = 'project'
+
+ # set from the querystring
+ self.project_id = None
+
+ def setup_columns(self, *args, **kwargs):
+ """
+ Project builds table doesn't show the machines column by default
+ """
+
+ super(ProjectBuildsTable, self).setup_columns(*args, **kwargs)
+
+ # hide the machine column
+ self.set_column_hidden('Machine', True)
+
+ # allow the machine column to be hidden by the user
+ self.set_column_hideable('Machine', True)
+
+ def setup_queryset(self, *args, **kwargs):
+ """
+ NOTE: self.project_id must be set before calling super(),
+ as it's used in setup_queryset()
+ """
+ self.project_id = kwargs['pid']
+ super(ProjectBuildsTable, self).setup_queryset(*args, **kwargs)
+
+ project = Project.objects.get(pk=self.project_id)
+ self.queryset = self.queryset.filter(project=project)
+
+ def get_context_data(self, **kwargs):
+ """
+ Get recent builds for this project, and the project itself
+
+ NOTE: self.project_id must be set before calling super(),
+ as it's used in get_context_data()
+ """
+ self.project_id = kwargs['pid']
+ context = super(ProjectBuildsTable, self).get_context_data(**kwargs)
+
+ project = Project.objects.get(pk=self.project_id)
+ context['mru'] = Build.get_recent(project)
+ context['project'] = project
+
+ return context
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/base.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/base.html
index 11ac2a035..210cf3360 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/base.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/base.html
@@ -3,15 +3,15 @@
{% load projecttags %}
{% load project_url_tag %}
<html lang="en">
- <head>
- <title>
- {% block title %} Toaster {% endblock %}
- </title>
-<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" type="text/css"/>
-<link rel="stylesheet" href="{% static 'css/bootstrap-responsive.min.css' %}" type='text/css'/>
-<link rel="stylesheet" href="{% static 'css/font-awesome.min.css' %}" type='text/css'/>
-<link rel="stylesheet" href="{% static 'css/prettify.css' %}" type='text/css'/>
-<link rel="stylesheet" href="{% static 'css/default.css' %}" type='text/css'/>
+ <head>
+ <title>
+ {% block title %} Toaster {% endblock %}
+ </title>
+ <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" type="text/css"/>
+ <link rel="stylesheet" href="{% static 'css/bootstrap-responsive.min.css' %}" type='text/css'/>
+ <link rel="stylesheet" href="{% static 'css/font-awesome.min.css' %}" type='text/css'/>
+ <link rel="stylesheet" href="{% static 'css/prettify.css' %}" type='text/css'/>
+ <link rel="stylesheet" href="{% static 'css/default.css' %}" type='text/css'/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
@@ -40,48 +40,35 @@
projectId : {{project.id}},
projectPageUrl : {% url 'project' project.id as purl %}{{purl|json}},
projectName : {{project.name|json}},
- projectIsDefault: {% if project.is_default %}true{% else %}false{% endif %},
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}},
+ xhrBuildRequestUrl: "{% url 'xhr_buildrequest' project.id %}",
{% else %}
projectId : undefined,
projectPageUrl : undefined,
projectName : undefined,
- projectIsDefault: false,
{% endif %}
};
</script>
- <script src="{% static 'js/base.js' %}"></script>
- <script>
- $(document).ready(function () {
- /* Vars needed for base.js */
- var ctx = {};
- ctx.numProjects = {{projects|length}};
- ctx.currentUrl = "{{request.path|escapejs}}";
-
- basePageInit(ctx);
- });
- </script>
+ {% block extraheadcontent %}
+ {% endblock %}
+ </head>
-{% block extraheadcontent %}
-{% endblock %}
- </head>
+ <body style="height: 100%">
-<body style="height: 100%">
-
- {% csrf_token %}
- <div id="loading-notification" class="alert lead text-center" style="display:none">
- Loading <i class="fa-pulse icon-spinner"></i>
- </div>
+ {% csrf_token %}
+ <div id="loading-notification" class="alert lead text-center" style="display:none">
+ Loading <i class="fa-pulse icon-spinner"></i>
+ </div>
- <div id="change-notification" class="alert lead alert-info" style="display:none">
- <button type="button" class="close" id="hide-alert">&times;</button>
- <span id="change-notification-msg"></span>
- </div>
+ <div id="change-notification" class="alert lead alert-info" style="display:none">
+ <button type="button" class="close" id="hide-alert">&times;</button>
+ <span id="change-notification-msg"></span>
+ </div>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
@@ -90,10 +77,10 @@
<span class="brand">
<a href="/">Toaster</a>
{% if DEBUG %}
- <i class="icon-info-sign" title="<strong>Toaster version information</strong>" data-content="<dl><dt>Branch</dt><dd>{{TOASTER_BRANCH}}</dd><dt>Revision</dt><dd>{{TOASTER_REVISION}}</dd></dl>"></i>
+ <i class="icon-info-sign" title="<strong>Toaster version information</strong>" data-content="<dl><dt>Git branch</dt><dd>{{TOASTER_BRANCH}}</dd><dt>Git revision</dt><dd>{{TOASTER_REVISION}}</dd></dl>"></i>
{% endif %}
</span>
- {% if BUILD_MODE and request.resolver_match.url_name != 'landing' and request.resolver_match.url_name != 'newproject' %}
+ {% if request.resolver_match.url_name != 'landing' and request.resolver_match.url_name != 'newproject' %}
<ul class="nav">
<li {% if request.resolver_match.url_name == 'all-builds' %}
class="active"
@@ -122,75 +109,18 @@
</li>
</ul>
<span class="pull-right divider-vertical"></span>
-
- <!-- new project button; only show in build mode -->
- {% if BUILD_MODE %}
- <div class="btn-group pull-right">
- <a class="btn" id="new-project-button" href="{% url 'newproject' %}">New project</a>
- </div>
- {% endif %}
-
- <!--
- New build popover; only shown if there is at least one user-created project
- and we're in build mode
- -->
- {% if BUILD_MODE and non_cli_projects.count > 0 %}
- <div class="btn-group pull-right" id="new-build-button" style="display:none">
- <button class="btn dropdown-toggle" data-toggle="dropdown">
- New build
- <i class="icon-caret-down"></i>
- </button>
- <ul class="dropdown-menu new-build multi-select">
- <li>
- <h3>New build</h3>
- <h6>
- Project:
- <span id="project">
- {% if project.id and not project.is_default %}
- <a class="lead" href="{% project_url project %}">{{project.name}}</a>
- {% else %}
- <a class="lead" href="#"></a>
- {% endif %}
- <i class="icon-pencil"></i>
- </span>
- </h6>
- <form id="change-project-form" style="display:none;">
- <div class="input-append">
- <input type="text" class="input-medium" id="project-name-input" placeholder="Type a project name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead"/>
- <button id="save-project-button" class="btn" type="button">Save</button>
- <a href="#" id="cancel-change-project" class="btn btn-link" style="display: none">Cancel</a>
- </div>
- <p><a id="view-all-projects" href="{% url 'all-projects' %}">View all projects</a></p>
- </form>
- </li>
- <li>
- <div class="alert" style="display:none;">
- <p>This project configuration is incomplete, so you cannot run builds.</p>
- <p><a href="{% if project.id %}{% url 'project' project.id %}{% endif %}">View project configuration</a></p>
- </div>
- </li>
- <li id="targets-form">
- <h6>Recipe(s):</h6>
- <form>
- <input type="text" class="input-xlarge build-target-input" placeholder="Type a recipe name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead" disabled/>
- <div class="row-fluid">
- <button class="btn btn-primary build-button" disabled>Build</button>
- </div>
- </form>
- </li>
- </ul>
- </div>
- {% endif %}
+ <div class="btn-group pull-right">
+ <a class="btn" id="new-project-button" href="{% url 'newproject' %}">New project</a>
+ </div>
+ </div>
+ </div>
</div>
- </div>
-</div>
-<div class="container-fluid top-padded">
-<div class="row-fluid">
-{% block pagecontent %}
-{% endblock %}
-</div>
-</div>
-</body>
+ <div class="container-fluid top-padded">
+ <div class="row-fluid">
+ {% block pagecontent %}
+ {% endblock %}
+ </div>
+ </div>
+ </body>
</html>
-
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/basebuilddetailpage.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/basebuilddetailpage.html
index 22ca50c0c..a62e0b1cb 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/basebuilddetailpage.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/basebuilddetailpage.html
@@ -1,4 +1,5 @@
{% extends "base.html" %}
+{% load project_url_tag %}
{% load humanize %}
{% block pagecontent %}
@@ -6,16 +7,18 @@
<!-- Breadcrumbs -->
<div class="section">
<ul class="breadcrumb" id="breadcrumb">
- <li class="muted">{{build.project.name}}:</li>
- <li><a href="{% url 'projectbuilds' build.project.id %}">Builds</a></li>
- <li><a href="{%url 'builddashboard' build.pk%}">{{build.target_set.all.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} ({{build.completed_on|date:"d/m/y H:i"}})</a></li>
+ <li><a href="{% project_url build.project %}">{{build.project.name}}</a></li>
+ {% if not build.project.is_default %}
+ <li><a href="{% url 'projectbuilds' build.project.id %}">Builds</a></li>
+ {% endif %}
+ <li><a href="{%url 'builddashboard' build.pk%}">{{build.get_sorted_target_list.0.target}} {%if build.target_set.all.count > 1%}(+{{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}})</a></li>
{% block localbreadcrumb %}{% endblock %}
</ul>
<script>
$( function () {
$('#breadcrumb > li').append('<span class="divider">&rarr;</span>');
$('#breadcrumb > li:last').addClass("active");
- $('#breadcrumb > li:last > span, #breadcrumb > li:first > span').remove();
+ $('#breadcrumb > li:last > span').remove();
});
</script>
</div> <!--section-->
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/basebuildpage.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/basebuildpage.html
index d441df84e..0d8c8820d 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/basebuildpage.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/basebuildpage.html
@@ -1,88 +1,157 @@
{% extends "base.html" %}
{% load projecttags %}
+{% load project_url_tag %}
+{% load objects_to_dictionaries_filter %}
{% load humanize %}
{% block pagecontent %}
+ <!-- breadcrumbs -->
+ <div class="section">
+ <ul class="breadcrumb" id="breadcrumb">
+ <li><a href="{% project_url build.project %}">{{build.project.name}}</a></li>
+ {% if not build.project.is_default %}
+ <li><a href="{% url 'projectbuilds' build.project.id %}">Builds</a></li>
+ {% endif %}
+ <li>
+ {% block parentbreadcrumb %}
+ <a href="{%url 'builddashboard' build.pk%}">
+ {{build.get_sorted_target_list.0.target}} {% if build.target_set.all.count > 1 %}(+{{build.target_set.all.count|add:"-1"}}){% endif %} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}})
+ </a>
+ {% endblock %}
+ </li>
+ {% block localbreadcrumb %}{% endblock %}
+ </ul>
+ <script>
+ $( function () {
+ $('#breadcrumb > li').append('<span class="divider">&rarr;</span>');
+ $('#breadcrumb > li:last').addClass("active");
+ $('#breadcrumb > li:last > span').remove();
+ });
+ </script>
+ </div>
+ <div class="row-fluid">
+ <!-- begin left sidebar container -->
+ <div id="nav" class="span2">
+ <ul class="nav nav-list well">
+ <li
+ {% if request.resolver_match.url_name == 'builddashboard' %}
+ class="active"
+ {% endif %} >
+ <a class="nav-parent" href="{% url 'builddashboard' build.pk %}">Build summary</a>
+ </li>
+ {% if build.target_set.all.0.is_image and build.outcome == 0 %}
+ <li class="nav-header">Images</li>
+ {% block nav-target %}
+ {% for t in build.get_sorted_target_list %}
+ <li><a href="{% url 'target' build.pk t.pk %}">{{t.target}}</a><li>
+ {% endfor %}
+ {% endblock %}
+ {% endif %}
+ <li class="nav-header">Build</li>
+ {% block nav-configuration %}
+ <li><a href="{% url 'configuration' build.pk %}">Configuration</a></li>
+ {% endblock %}
+ {% block nav-tasks %}
+ <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li>
+ {% endblock %}
+ {% block nav-recipes %}
+ <li><a href="{% url 'recipes' build.pk %}">Recipes</a></li>
+ {% endblock %}
+ {% block nav-packages %}
+ <li><a href="{% url 'packages' build.pk %}">Packages</a></li>
+ {% endblock %}
+ <li class="nav-header">Performance</li>
+ {% block nav-buildtime %}
+ <li><a href="{% url 'buildtime' build.pk %}">Time</a></li>
+ {% endblock %}
+ {% block nav-cputime %}
+ <li><a href="{% url 'cputime' build.pk %}">CPU usage</a></li>
+ {% endblock %}
+ {% block nav-diskio %}
+ <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li>
+ {% endblock %}
- <div class="">
-<!-- Breadcrumbs -->
- <div class="section">
- <ul class="breadcrumb" id="breadcrumb">
- <li class="muted">{{build.project.name}}:</li>
- <li><a href="{% url 'projectbuilds' build.project.id %}">Builds</a></li>
- <li>
- {% block parentbreadcrumb %}
- <a href="{%url 'builddashboard' build.pk%}">
- {{build.get_sorted_target_list.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} ({{build.completed_on|date:"d/m/y H:i"}})
+ <li class="divider"></li>
+
+ <li>
+ <p class="navbar-btn">
+ <a class="btn btn-block" href="{% url 'build_artifact' build.id 'cookerlog' build.id %}">
+ Download build log
</a>
- {% endblock %}
+ </p>
+ </li>
+
+ {% 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 }};
+
+ $(document).ready(function () {
+ var editCustomImageTrigger = $('[data-role="edit-custom-image-trigger"]');
+ var editCustomImageModal = $('#edit-custom-image-modal');
+
+ // 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>
- {% block localbreadcrumb %}{% endblock %}
- </ul>
- <script>
- $( function () {
- $('#breadcrumb > li').append('<span class="divider">&rarr;</span>');
- $('#breadcrumb > li:last').addClass("active");
- $('#breadcrumb > li:last > span, #breadcrumb > li:first > span').remove();
- console.log("done");
- });
- </script>
- </div>
+ {% endif %}
+ {% endwith %}
- <div class="row-fluid">
+ <li>
+ <!-- new custom image from image recipe in this build -->
+ <p class="navbar-btn" data-role="new-custom-image-trigger">
+ <button class="btn btn-block">New custom image</button>
+ </p>
+ {% include 'newcustomimage_modal.html' %}
+ <script>
+ // 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 | objects_to_dictionaries:"id,name" | json }};
- <!-- begin left sidebar container -->
- <div id="nav" class="span2">
- <ul class="nav nav-list well">
- <li
- {% if request.resolver_match.url_name == 'builddashboard' %}
- class="active"
- {% endif %} >
- <a class="nav-parent" href="{% url 'builddashboard' build.pk %}">Build summary</a>
- </li>
- {% if build.target_set.all.0.is_image and build.outcome == 0 %}
- <li class="nav-header">Images</li>
- {% block nav-target %}
- {% for t in build.get_sorted_target_list %}
- <li><a href="{% url 'target' build.pk t.pk %}">{{t.target}}</a><li>
- {% endfor %}
- {% endblock %}
- {% endif %}
- <li class="nav-header">Build</li>
- {% block nav-configuration %}
- <li><a href="{% url 'configuration' build.pk %}">Configuration</a></li>
- {% endblock %}
- {% block nav-tasks %}
- <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li>
- {% endblock %}
- {% block nav-recipes %}
- <li><a href="{% url 'recipes' build.pk %}">Recipes</a></li>
- {% endblock %}
- {% block nav-packages %}
- <li><a href="{% url 'packages' build.pk %}">Packages</a></li>
- {% endblock %}
- <li class="nav-header">Performance</li>
- {% block nav-buildtime %}
- <li><a href="{% url 'buildtime' build.pk %}">Time</a></li>
- {% endblock %}
- {% block nav-cpuusage %}
- <li><a href="{% url 'cpuusage' build.pk %}">CPU usage</a></li>
- {% endblock %}
- {% block nav-diskio %}
- <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li>
- {% endblock %}
- </ul>
- </div>
- <!-- end left sidebar container -->
+ $(document).ready(function () {
+ var newCustomImageModal = $('#new-custom-image-modal');
+ var newCustomImageTrigger = $('[data-role="new-custom-image-trigger"]');
- <!-- Begin right container -->
- {% block buildinfomain %}{% endblock %}
- <!-- End right container -->
+ // show create new custom image modal to select an image built
+ // during this build as the basis for the custom recipe
+ newCustomImageTrigger.click(function () {
+ if (!imageRecipes.length) {
+ return;
+ }
+ newCustomImageModalSetRecipes(imageRecipes);
+ newCustomImageModal.modal('show');
+ });
+ });
+ </script>
+ </li>
+ </ul>
</div>
- </div>
-
+ <!-- end left sidebar container -->
+ <!-- begin right container -->
+ {% block buildinfomain %}{% endblock %}
+ <!-- end right container -->
+ </div>
{% endblock %}
-
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html
index 1f45be462..8778305f3 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html
@@ -1,4 +1,5 @@
{% extends "base.html" %}
+
{% load projecttags %}
{% load humanize %}
@@ -26,9 +27,7 @@
<ul class="nav nav-list well">
<li><a class="nav-parent" href="{% url 'project' project.id %}">Configuration</a></li>
<li class="nav-header">Compatible metadata</li>
- {% if CUSTOM_IMAGE %}
<li><a href="{% url 'projectcustomimages' project.id %}">Custom images</a></li>
- {% endif %}
<li><a href="{% url 'projectimagerecipes' project.id %}">Image recipes</a></li>
<li><a href="{% url 'projectsoftwarerecipes' project.id %}">Software recipes</a></li>
<li><a href="{% url 'projectmachines' project.id %}">Machines</a></li>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/basetable_top.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/basetable_top.html
index 33ede6687..0ddd74904 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/basetable_top.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/basetable_top.html
@@ -161,8 +161,8 @@
<!-- control header -->
<div class="navbar">
<div class="navbar-inner">
- <form class="navbar-search input-append pull-left" id="searchform">
- <input class="input-xxlarge" id="search" name="search" type="text" placeholder="Search {%if object_search_display %}{{object_search_display}}{%else%}{{objectname}}{%endif%}" value="{%if request.GET.search %}{{request.GET.search}}{% endif %}"/>{% if request.GET.search %}<a href="javascript:$('#search').val('');searchform.submit()" class="add-on btn" tabindex="-1"><i class="icon-remove"></i></a>{%endif%}
+ <form class="navbar-search input-append pull-left span6" id="searchform">
+ <input id="search" name="search" type="text" placeholder="Search {%if object_search_display %}{{object_search_display}}{%else%}{{objectname}}{%endif%}" value="{%if request.GET.search %}{{request.GET.search}}{% endif %}"/>{% if request.GET.search %}<a href="javascript:$('#search').val('');searchform.submit()" class="add-on btn" tabindex="-1"><i class="icon-remove"></i></a>{%endif%}
<input type="hidden" name="orderby" value="{{request.GET.orderby}}">
<input type="hidden" name="page" value="1">
<button class="btn" id="search-button" type="submit" value="Search">Search</button>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/builddashboard.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/builddashboard.html
index 323bbbb6e..a0da71ea4 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/builddashboard.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/builddashboard.html
@@ -6,10 +6,9 @@
{% block parentbreadcrumb %}
{% if build.get_sorted_target_list.count > 0 %}
{{build.get_sorted_target_list.0.target}}
- &nbsp;
{% endif %}
-{%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}})
+{%if build.target_set.all.count > 1%}(+{{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}})
{% endblock %}
{% block buildinfomain %}
@@ -111,7 +110,7 @@
</p>
</div>
</div>
- {% else %}
+ {% else %}
<dt>
<i class="icon-question-sign get-help" title="The location in disk of the license manifest, a document listing all packages installed in your image and their licenses"></i>
@@ -126,18 +125,18 @@
</dt>
<dd>
<ul>
- {% for i in target.imageFiles %}
- {% if build.project %}
- <li><a href="{% url 'build_artifact' build.pk 'imagefile' i.id %}">{{i.path}}</a>
- {% else %}
- <li>{{i.path}}
- {% endif %}
- ({{i.size|filtered_filesizeformat}})</li>
- {% endfor %}
+ {% for i in target.imageFiles %}
+ <li>
+ <a href="{% url 'build_artifact' build.pk 'imagefile' i.id %}">
+ {{i.suffix}}
+ </a>
+ &nbsp;({{i.size|filtered_filesizeformat}})
+ </li>
+ {% endfor %}
</ul>
</dd>
</dl>
- {% endif %}
+ {% endif %}
</div>
{% endif %}
{% endfor %}
@@ -161,7 +160,7 @@
<dd><div>
{% for ba in build.buildartifact_set.all|dictsort:"file_name" %}
<a href="{%url 'build_artifact' build.id 'buildartifact' ba.id %}">
- {{ba.get_local_file_name}}
+ {{ba.get_basename}}
</a>
({{ba.file_size|filtered_filesizeformat}}) <br/>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
new file mode 100644
index 000000000..bf13a66bd
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
@@ -0,0 +1,48 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extraheadcontent %}
+ <link rel="stylesheet" href="{% static 'css/jquery-ui.min.css' %}" type='text/css'>
+ <link rel="stylesheet" href="{% static 'css/jquery-ui.structure.min.css' %}" type='text/css'>
+ <link rel="stylesheet" href="{% static 'css/jquery-ui.theme.min.css' %}" type='text/css'>
+ <script src="{% static 'js/jquery-ui.min.js' %}">
+ </script>
+{% endblock %}
+
+{% block title %} All builds - Toaster {% endblock %}
+
+{% block pagecontent %}
+
+ <div class="row-fluid">
+ {% with mru=mru mrb_type=mrb_type %}
+ {% include 'mrb_section.html' %}
+ {% endwith %}
+
+ <h1 class="page-header top-air" data-role="page-title"></h1>
+
+ {% url 'builds' as xhr_table_url %}
+ {% include 'toastertable.html' %}
+ </div>
+
+ <script>
+ $(document).ready(function () {
+ var tableElt = $("#{{table_name}}");
+ var titleElt = $("[data-role='page-title']");
+
+ tableElt.on("table-done", function (e, total, tableParams) {
+ var title = "All builds";
+
+ if (tableParams.search || tableParams.filter) {
+ if (total === 0) {
+ title = "No builds found";
+ }
+ else if (total > 0) {
+ title = total + " build" + (total > 1 ? 's' : '') + " found";
+ }
+ }
+
+ titleElt.text(title);
+ });
+ });
+ </script>
+{% endblock %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/builds.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/builds.html
deleted file mode 100644
index a27a12191..000000000
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/builds.html
+++ /dev/null
@@ -1,119 +0,0 @@
-{% extends "base.html" %}
-
-{% load static %}
-{% load projecttags %}
-{% load project_url_tag %}
-{% load humanize %}
-
-{% block title %} All builds - Toaster {% endblock %}
-{% block extraheadcontent %}
-<link rel="stylesheet" href="/static/css/jquery-ui.min.css" type='text/css'>
-<link rel="stylesheet" href="/static/css/jquery-ui.structure.min.css" type='text/css'>
-<link rel="stylesheet" href="/static/css/jquery-ui.theme.min.css" type='text/css'>
-<script src="/static/js/jquery-ui.min.js"></script>
-<script src="/static/js/filtersnippet.js"></script>
-{% endblock %}
-
-{% block pagecontent %}
-
-{% if last_date_from and last_date_to %}
-<script>
- // initialize the date range controls
- $(document).ready(function () {
- date_init('started_on','{{last_date_from}}','{{last_date_to}}','{{dateMin_started_on}}','{{dateMax_started_on}}','{{daterange_selected}}');
- date_init('completed_on','{{last_date_from}}','{{last_date_to}}','{{dateMin_completed_on}}','{{dateMax_completed_on}}','{{daterange_selected}}');
- });
-</script>
-{%endif%} {# last_date_from and last_date_to #}
-
-<div class="row-fluid">
-
- {% include "mrb_section.html" %}
-
- <div class="page-header top-air">
- <h1>
- {% if request.GET.filter and objects.paginator.count > 0 or request.GET.search and objects.paginator.count > 0 %}
- {{objects.paginator.count}} build{{objects.paginator.count|pluralize}} found
- {%elif request.GET.filter and objects.paginator.count == 0 or request.GET.search and objects.paginator.count == 0 %}
- No builds found
- {%else%}
- All builds
- {%endif%}
- </h1>
- </div>
-
- {% if objects.paginator.count == 0 %}
- <div class="row-fluid">
- <div class="alert">
- <form class="no-results input-append" id="searchform">
- <input id="search" name="search" class="input-xxlarge" type="text" value="
- {% if request.GET.search %}
- {{request.GET.search}}
- {% endif %}"/>
- {% if request.GET.search %}<a href="javascript:$('#search').val('');searchform.submit()" class="add-on btn" tabindex="-1"><i class="icon-remove"></i></a>{% endif %}
- <button class="btn" type="submit" value="Search">Search</button>
- <button class="btn btn-link" onclick="javascript:$('#search').val('');searchform.submit()">Show all builds</button>
- </form>
- </div>
- </div>
- {% else %}
- {% include "basetable_top.html" %}
- <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work -->
- {% for build in objects %}
- <tr class="data" data-table-build-result="{{ build.id }}">
- <td class="outcome">
- <a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a> &nbsp;
- </td>
- <td class="target">
- {% for t in build.target_set.all %}
- <a href="{% url "builddashboard" build.id %}">
- {% if t.task %}
- {{t.target}}:{{t.task}}
- {% else %}
- {{t.target}}
- {% endif %}
- </a> <br />
- {% endfor %}
- </td>
- <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td>
- <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td>
- <td class="completed_on"><a href="{% url "builddashboard" build.id %}">{{build.completed_on|date:"d/m/y H:i"}}</a></td>
- <td class="failed_tasks error">
- {% query build.task_build outcome=4 order__gt=0 as exectask%}
- {% if exectask.count == 1 %}
- <a href="{% url "task" build.id exectask.0.id %}">{{exectask.0.recipe.name}}.{{exectask.0.task_name}}</a>
- <a href="{% url 'build_artifact' build.id "tasklogfile" exectask.0.id %}">
- <i class="icon-download-alt" title="" data-original-title="Download task log file"></i>
- </a>
- {% elif exectask.count > 1%}
- <a href="{% url "tasks" build.id %}?filter=outcome%3A4">{{exectask.count}} task{{exectask.count|pluralize}}</a>
- {%endif%}
- </td>
- <td class="errors.count errors_no">
- {% if build.errors.count %}
- <a class="errors.count error" href="{% url "builddashboard" build.id %}#errors">{{build.errors.count}} error{{build.errors.count|pluralize}}</a>
- {%endif%}
- </td>
- <td class="warnings.count warnings_no">{% if build.warnings.count %}<a class="warnings.count warning" href="{% url "builddashboard" build.id %}#warnings">{{build.warnings.count}} warning{{build.warnings.count|pluralize}}</a>{%endif%}</td>
- <td class="time"><a href="{% url "buildtime" build.id %}">{{build.timespent_seconds|sectohms}}</a></td>
- <td class="output">
- {% if build.outcome == build.SUCCEEDED %}
- <a href="{%url "builddashboard" build.id%}#images">{{fstypes|get_dict_value:build.id}}</a>
- {% endif %}
- </td>
- <td class="project-name">
- <a href="{% project_url build.project %}">{{build.project.name}}</a>
- {% if build.project.is_default %}
- <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project shows information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i>
- {% endif %}
- </td>
- </tr>
-
- {% endfor %}
-
-
- {% include "basetable_bottom.html" %}
- {% endif %} {# objects.paginator.count #}
-</div><!-- end row-fluid-->
-
-{% endblock %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html
index 54d05f9ea..2e54a9d90 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html
@@ -1,9 +1,15 @@
-<button class="btn btn-block layer-exists-{{data.layer_version.id}} customise-btn" style="display:none;" data-recipe="{{data.id}}">
+<button class="btn btn-block layer-exists-{{data.layer_version.pk}} customise-btn" data-recipe="{{data.pk}}"
+ {% if data.layer_version.pk not in extra.current_layers %}
+ style="display:none;"
+ {% endif %}
+ >
Customise
</button>
-
-<button class="btn btn-block layer-add-{{data.layer_version.id}} layerbtn" data-layer='{ "id": {{data.layer_version.id}}, "name": "{{data.layer_version.layer.name}}", "layerdetailurl": "{% url 'layerdetails' extra.pid data.layer_version.id %}"}' data-directive="add">
+<button class="btn btn-block layer-add-{{data.layer_version.pk}} layerbtn" data-layer='{ "id": {{data.layer_version.pk}}, "name": "{{data.layer_version.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.layer_version.pk%}"}' data-directive="add"
+ {% if data.layer_version.pk in extra.current_layers %}
+ style="display:none;"
+ {% endif %}
+ >
<i class="icon-plus"></i>
Add layer
</button>
-
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/customrecipe.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/customrecipe.html
index 823bbd8a1..ea3c9c732 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/customrecipe.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/customrecipe.html
@@ -4,13 +4,32 @@
{% load static %}
{% block pagecontent %}
-{% include "projecttopbar.html" %}
+<div class="section">
+ <ul class="breadcrumb">
+ <li>
+ <a href="{% url 'project' project.id %}">{{project.name}}</a>
+ <span class="divider">&rarr;</span>
+ </li>
+ <li><a href="{% url 'projectcustomimages' project.id %}">Custom images</a>
+ <span class="divider">&rarr;</span>
+ </li>
+ <li class="active">
+ {{recipe.name}} ({{recipe.layer_version.layer.name}})
+ </li>
+ </ul>
+</div>
<script src="{% static 'js/customrecipe.js' %}"></script>
<script>
$(document).ready(function (){
var ctx = {
- tableApiUrl: "{% url 'recipeselectpackages' project.id recipe.pk %}?format=json"
+ recipe : {
+ id: {{recipe.pk}},
+ name: "{{recipe.name}}",
+ includedPackagesCount: {{recipe.includes_set.count}},
+ baseRecipeId: {{recipe.base_recipe.pk}},
+ xhrPackageListUrl: "{% url 'xhr_customrecipe_packages' recipe.pk %}",
+ }
};
try {
@@ -21,6 +40,45 @@
}
});
</script>
+<!-- package dependencies modal -->
+<div style="display:none" id="package-deps-modal" class="modal hide fade in" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="false">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
+ <h3><span class="package-to-add-name"></span> dependencies</h3>
+ </div>
+ <div class="modal-body">
+ <p>Based on information from a previous build it is likely that adding <strong class="package-to-add-name"></strong> will also add the following packages to your custom image:</p>
+ <ul id="package-add-dep-list">
+ </ul>
+ </div>
+ <div class="modal-footer">
+ <p class="help-block text-left">Total package size: <strong id="package-deps-total-size"></strong></p>
+ <button id="add-package-deps-modal-btn" type="submit" class="btn btn-primary" data-dismiss="modal">Add package</button>
+ <button class="btn" data-dismiss="modal">Cancel</button>
+ </div>
+</div>
+<!-- end package dependencies modal -->
+
+<!-- package reverse dependencies modal -->
+<div style="display:none" id="package-reverse-deps-modal" class="modal hide fade in" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="false">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
+ <h3><span class="package-to-rm-name"></span> reverse dependencies</h3>
+ </div>
+ <div class="modal-body">
+ <p> <strong class="reverse-deps-count"></strong> <span class="reverse-deps-package"></span> in your image <span class="reverse-deps-depends"></span> on <strong class="package-to-rm-name"></strong>:</p>
+ <ul id="package-reverse-dep-list">
+ </ul>
+ <p>In order to remove <strong class="package-to-rm-name"></strong>, you must remove <span class="reverse-deps-this"></span> <strong class="reverse-deps-count"></strong> <span class="reverse-deps-package"></span> as well.</p>
+ </div>
+ <div class="modal-footer">
+ <p class="help-block text-left">Total package size: <strong id="package-reverse-deps-total-size"></strong></p>
+ <button id="rm-package-reverse-deps-modal-btn" type="submit" class="btn btn-primary" data-dismiss="modal">Remove all <span class="reverse-deps-count-plus1"></button>
+ <button class="btn" data-dismiss="modal">Cancel</button>
+ </div>
+</div>
+<!-- end package dependencies modal -->
+
<div class="row-fluid span11">
<div class="alert alert-success lead" id="image-created-notification" style="margin-top: 15px; display: none">
@@ -30,7 +88,7 @@
<div class="page-header air">
<h1>
{{recipe.name}}
- <small>({{recipe.base_recipe.name}})</small>
+ <small>({{recipe.layer_version.layer.name}})</small>
</h1>
</div>
</div>
@@ -38,44 +96,49 @@
<div class="row-fluid span11">
<div class="span8">
<div class="button-place btn-group" style="width: 100%">
- <a class="btn btn-large span6" href="#" id="build-custom-image" style="width: 50%">
+ <a class="btn btn-large span6 build-custom-image" href="#" style="width: 50%">
Build {{recipe.name}}
</a>
- <button class="btn btn-large span6" data-toggle="modal" data-target="#download-file" id="download" style="width: 50%">
- Download recipe file
- </button>
- </div>
- <div id="no-package-results" class="air" style="display:none;">
- <div class="alert">
- <h3>No packages found</h3>
- <p>You might consider <a href="all-software-recipes.html">searching the list of recipes</a> instead. If you find a recipe that matches the name of the package you want:</p>
- <ol>
- <li>Add the layer providing the recipe to your project</li>
- <li>Build the recipe</li>
- <li>Once the build completes, come back to this page and search for the package</li>
- </ol>
- <form class="input-append no-results">
- <input type="text" class="input-xlarge" value="search query">
- <a href="#" class="add-on btn">
+ <a href="{% url 'customrecipedownload' project.id recipe.id %}" class="btn btn-large span6" style="width: 50%">
+ Download recipe file
+ </a>
+ </div>
+ <div id="no-results-special-{{table_name}}" class="air" style="display:none;">
+ <div class="alert">
+ <h3>No packages found</h3>
+ <p>You might consider <a href={% url 'projectsoftwarerecipes' project.id %}>searching the list of recipes</a> instead. If you find a recipe that matches the name of the package you want:</p>
+ <ol>
+ <li>Add the layer providing the recipe to your project</li>
+ <li>Build the recipe</li>
+ <li>Once the build completes, come back to this page and search for the package</li>
+ </ol>
+ <form class="input-append no-results">
+ <input type="text" class="input-xlarge no-results-search-input" id="no-results-search-input-{{table_name}}" name="search" placeholder="Search {{title|lower}}" />
+ <a href="#" class="add-on btn" id="no-results-remove-search-btn" tabindex="-1">
<i class="icon-remove"></i>
</a>
- <button class="btn">Search</button>
- <button class="btn btn-link" id="show-all">Show all packages</button>
+ <button class="btn search-submit-{{table_name}}">Search</button>
+ <button class="btn btn-link" id="no-results-show-all-packages">Show all packages</button>
</form>
</div>
+ </div>
+ <div id="results-found-{{table_name}}">
+ <div id="packages-table">
+ {% if recipe.get_all_packages.count == 0 and last_build == None %}
+ <h2> Add | Remove packages </h2>
+ <div class="alert alert-info air">
+ <p class="lead">Toaster has no package information for {{recipe.name}}. To generate package information, build {{recipe.name}}</p>
+ <button class="btn btn-info btn-large build-custom-image" style="margin:20px 0 10px 0;">Build {{recipe.name}}</button>
+ </div>
+ {% else %}
+ {# ToasterTable for Adding remove packages #}
+ {% url 'recipeselectpackages' project.id recipe.id as xhr_table_url %}
+ <h2>{{title}} (<span class="table-count-{{table_name}}">0</span>) </h2>
+ {% include "toastertable.html" %}
+ {% endif %}
+ </div>
+ </div>
</div>
- <div id="packages-table">
- {% url 'recipeselectpackages' project.id recipe.id as xhr_table_url %}
- {% with 'recipeselection' as table_name %}
- {% with 'Add | Remove packages' as title %}
-
- <h2>{{title}} (<span class="table-count-{{table_name}}"></span>) </h2>
-
- {% include "toastertable.html" %}
- {% endwith %}
- {% endwith %}
- </div>
- </div>
<div class="span4 well">
<h2 style="margin-bottom:20px;">About {{recipe.name}}</h2>
@@ -84,59 +147,77 @@
Approx. packages included
<i class="icon-question-sign get-help" title="" data-original-title="The number of packages included is based on information from previous builds and from parsing layers, so we can never be sure it is 100% accurate"></i>
</dt>
- <dd class="no-packages">{{recipe.packages.count}}</dd>
- <!-- <dt>
+ <dd id="total-num-packages">{{recipe.get_all_packages.count}}</dd>
+ <dt>
Approx. package size
<i class="icon-question-sign get-help" title="" data-original-title="Package size is based on information from previous builds, so we can never be sure it is 100% accurate"></i>
</dt>
- <dd>244.3 MB</dd>
+ <dd id="total-size-packages">{{approx_pkg_size.size__sum|filtered_filesizeformat}}</dd>
+ {% if last_build %}
<dt>Last build</dt>
<dd>
<i class="icon-ok-sign success"></i>
- <a href="build-dashboard.html">11/06/15 15:22</a>
+ <a href="{% url 'projectbuilds' project.id%}">{{last_build.completed_on|date:"d/m/y H:i"}}</a>
</dd>
+ {% endif %}
+ <dt>Layer</dt>
+ <dd><a href="{% url 'layerdetails' project.id recipe.layer_version.pk %}">{{recipe.layer_version.layer.name}}</a></dd>
+ <dt>Based on</dt>
+ <dd><a href="{% url 'recipedetails' project.id recipe.base_recipe.pk %}">{{recipe.base_recipe.name}}</a></dd>
+ {% if recipe.get_last_successful_built_target %}
+ {% with recipe.get_last_successful_built_target as last_build_target %}
+ <dt>Last build</dt>
+ <dd>
+ <i class="icon-ok-sign success"></i>
+ <a href="{% url 'builddashboard' last_build_target.build.pk %}">
+ {{last_build_target.build.completed_on|date:"d/m/y H:i"}}</a>
+ </dd>
+ {% endwith %}
+ {% endif %}
<dt>Recipe file</dt>
<dd>
- <code>custom-image-name.bb</code>
- <a href="#download-file" data-toggle="modal"><i class="icon-download-alt" title="" data-original-title="Download recipe file"></i></a>
- </dd> -->
+ <code>{{recipe.name}}_{{recipe.version}}.bb</code>
+ <a href="{% url 'customrecipedownload' project.pk recipe.pk %}"><i class="icon-share" title="" data-original-title="View recipe file"></i></a>
+ </dd>
<dt>Layer</dt>
- <!-- TODO recipe details page -->
- <dd><a href="{% url 'layerdetails' project.id recipe.base_recipe.layer_version.pk %}">{{recipe.base_recipe.layer_version.layer.name}}</a></dd>
- <!--<dt>
+ <dd><a href="{% url 'layerdetails' project.id recipe.layer_version.pk %}">{{recipe.layer_version.layer.name}}</a></dd>
+ {% if recipe.summary %}
+ <dt>
Summary
</dt>
<dd>
- <span class="muted">Not set</span>
- <i class="icon-pencil" data-original-title="" title=""></i>
+ {{recipe.summary}}
</dd>
+ {% endif %}
+ {% if recipe.description %}
<dt>
Description
</dt>
<dd>
- <span class="muted">Not set</span>
- <i class="icon-pencil" data-original-title="" title=""></i>
+ {{recipe.description}}
</dd>
+ {% endif %}
<dt>Version</dt>
<dd>
- 1.0
- <i class="icon-pencil" data-original-title="" title=""></i>
+ {{recipe.version}}
</dd>
+ {% if recipe.section %}
<dt>Section</dt>
<dd>
- base
- <i class="icon-pencil" data-original-title="" title=""></i>
- <i class="icon-trash" data-original-title="" title=""></i>
+ {{recipe.section}}
</dd>
+ {% endif %}
<dt>License</dt>
<dd>
- MIT
+ {{recipe.license}}
<i class="icon-question-sign get-help" title="" data-original-title="All custom images have their license set to MIT. This is because the license applies only to the recipe (.bb) file, and not to the image itself. To see which licenses apply to the image you must check the license manifest generated with each build"></i>
- </dd> -->
+ </dd>
</dl>
+ <!--
<i class="icon-trash no-tooltip"></i>
<a href="#" class="error" id="delete">Delete custom image</a>
+ -->
</div>
-</div>
+ </div>
{% endblock %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
new file mode 100644
index 000000000..8046c08fb
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/editcustomimage_modal.html
@@ -0,0 +1,71 @@
+<!--
+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>Which image do you want to edit?</h3>
+ </div>
+
+ <div class="modal-body">
+ <div class="row-fluid">
+ {% 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" 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/yocto-poky/bitbake/lib/toaster/toastergui/templates/importlayer.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/importlayer.html
index 033f0aede..1848f410e 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/importlayer.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/importlayer.html
@@ -89,7 +89,7 @@
<input type="text" id="layer-subdir">
<div class="control-group" id="layer-revision-ctrl">
- <label class="control-label project-form" for="layer-git-ref">Revision
+ <label class="control-label project-form" for="layer-git-ref">Git revision
<span class="icon-question-sign get-help" title="You can provide a Git branch, a tag or a commit SHA as the revision"></span>
</label>
<div class="controls">
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html
index 8d65f33cb..94ad4f8e9 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html
@@ -4,9 +4,9 @@
{% load static %}
{% block pagecontent %}
-<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.18.0.css" />
+<link rel="stylesheet" href="{% static 'css/qunit-1.18.0.css' %}" />
-<script src="//code.jquery.com/qunit/qunit-1.18.0.js"></script>
+<script src="{% static 'js/qunit-1.18.0.js' %}"></script>
<script src="{% static 'js/layerDepsModal.js' %}"></script>
<script src="{% static 'js/projectpage.js' %}"></script>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing.html
index cafaa1afa..a1b5cdce7 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing.html
@@ -6,9 +6,6 @@
{% block title %} Welcome to Toaster {% endblock %}
{% block pagecontent %}
-
- {% if BUILD_MODE %}
- <!-- build mode -->
<div class="container-fluid">
<div class="row-fluid">
<div class="hero-unit span12 well-transparent">
@@ -62,11 +59,4 @@
</div>
</div>
</div>
- {% else %}
- <!-- analysis mode -->
- <div class="alert alert-info lead top-air">
- Toaster has not recorded any builds yet. Run a build from the command line to see it here.
- </div>
- {% endif %}
-
{% endblock %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/layer_btn.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/layer_btn.html
index 314eec7cf..10de37d4b 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/layer_btn.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/layer_btn.html
@@ -4,7 +4,7 @@
{% endif %}
>
<i class="icon-trash"></i>
- Delete layer
+ Remove layer
</button>
<button class="btn btn-block layer-add-{{data.pk}} layerbtn" data-layer='{ "id": {{data.pk}}, "name": "{{data.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.pk%}"}' data-directive="add"
{% if data.pk in extra.current_layers %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html
index 7fe365da3..82be3703b 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html
@@ -8,9 +8,8 @@
<div class="section">
<ul class="breadcrumb">
- <li class="muted">{{project.name}}:</li>
<li>
- <a href="{% url 'project' project.id %}">Configuration</a>
+ <a href="{% url 'project' project.id %}">{{project.name}}</a>
<span class="divider">&rarr;</span>
</li>
<li><a href="{% url 'projectlayers' project.id %}">Compatible layers</a>
@@ -46,7 +45,6 @@
$(document).ready(function (){
var ctx = {
- projectBuildsUrl : "{% url 'projectbuilds' project.id %}",
xhrUpdateLayerUrl : "{% url 'xhr_updatelayer' %}",
layerVersion : {
name : "{{layerversion.layer.name}}",
@@ -108,7 +106,7 @@
{% else %}
<button id="add-remove-layer-btn" data-directive="remove" class="btn btn-block btn-large btn-danger">
<span class="icon-trash"></span>
- Delete the {{layerversion.layer.name}} layer from your project
+ Remove the {{layerversion.layer.name}} layer from your project
</button>
{% endif %}
</span>
@@ -156,7 +154,7 @@
</dd>
<dt>
<i class="icon-question-sign get-help" title="The Git branch, tag or commit"></i>
- Revision
+ Git revision
</dt>
<dd>
<span class="current-value">{{layerversion.get_vcs_reference}}</span>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/machine_btn.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/machine_btn.html
index d2cb55bab..7b08f6a9a 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/machine_btn.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/machine_btn.html
@@ -1,6 +1,14 @@
-<a href="{% url 'project' extra.pid %}?setMachine={{data.name}}" class="btn btn-block layer-exists-{{data.layer_version.id}}" style="margin-top: 5px; display:none">
+<a href="{% url 'project' extra.pid %}?setMachine={{data.name}}" class="btn btn-block layer-exists-{{data.layer_version.id}}"
+ {% if data.layer_version.pk not in extra.current_layers %}
+ style="display:none;"
+ {% endif %}
+>
Select machine</a>
-<button class="btn btn-block layerbtn layer-add-{{data.layer_version.id}}" data-layer='{ "id": {{data.layer_version.id}}, "name": "{{data.layer_version.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.layer_version.id %}"}' data-directive="add">
+<button class="btn btn-block layerbtn layer-add-{{data.layer_version.id}}" data-layer='{ "id": {{data.layer_version.id}}, "name": "{{data.layer_version.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.layer_version.id %}"}' data-directive="add"
+ {% if data.layer_version.pk in extra.current_layers %}
+ style="display:none;"
+ {% endif %}
+>
<i class="icon-plus"></i>
Add layer
<i title="" class="icon-question-sign get-help" data-original-title="To enable this machine, you must first add the {{data.layer_version.layer.name}} layer to your project"></i>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html
index bd8f99178..b5e798d7c 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html
@@ -2,11 +2,26 @@
{% load projecttags %}
{% load project_url_tag %}
{% load humanize %}
+<script src="{% static 'js/mrbsection.js' %}"></script>
+<script>
+ $(document).ready(function () {
+ var ctx = {
+ mrbType : "{{mrb_type}}",
+ }
+
+ try {
+ mrbSectionInit(ctx);
+ } catch (e) {
+ document.write("Sorry, An error has occurred loading this page");
+ console.warn(e);
+ }
+ });
+</script>
{%if mru and mru.count > 0%}
{%if mrb_type == 'project' %}
- <h2>
+ <h2 class="page-header">
Latest project builds
{% if project.is_default %}
@@ -14,7 +29,7 @@
{% endif %}
</h2>
{% else %}
- <div class="page-header">
+ <div class="page-header top-air">
<h1>
Latest builds
</h1>
@@ -38,8 +53,9 @@
{% if build.target_set.all.count > 0 %}
<span data-toggle="tooltip"
{% if build.target_set.all.count > 1 %}
- title="Targets:
- {% for target in build.target_set.all %}
+ {{build.get_sorted_target_list.0.target}}
+ title="Recipes:
+ {% for target in build.get_sorted_target_list %}
{% if target.task %}
{{target.target}}:{{target.task}}
{% else %}
@@ -49,12 +65,12 @@
{% endif %}
>
{% if build.target_set.all.0.task %}
- {{build.target_set.all.0.target}}:{{build.target_set.all.0.task}}
+ {{build.get_sorted_target_list.0.target}}:{{build.target_set.all.0.task}}
{% else %}
- {{build.target_set.all.0.target}}
+ {{build.get_sorted_target_list.0.target}}
{% endif %}
{% if build.target_set.all.count > 1 %}
- (+ {{build.target_set.all.count|add:"-1"}})
+ (+{{build.target_set.all.count|add:"-1"}})
{% endif %}
</span>
{% endif %}
@@ -98,7 +114,7 @@
" title="Builds in this project cannot be started from Toaster: they are started from the command line">
</i>
{% else %}
- <button class="btn
+ <button class="run-again-btn btn
{% if build.outcome == build.SUCCEEDED %}
btn-success
{% elif build.outcome == build.FAILED %}
@@ -107,54 +123,56 @@
btn-info
{%endif%}
pull-right"
- onclick='scheduleBuild({% url 'projectbuilds' build.project.id as bpi %}{{bpi|json}},
- {{build.project.name|json}},
- {% url 'project' build.project.id as purl %}{{purl|json}},
- {{build.target_set.all|get_tasks|json}})'>
+ data-request-url="{% url 'xhr_buildrequest' build.project.pk %}"
+ data-target='{{build.target_set.all|get_tasks|json}}'>
+
- Run again
+ Rebuild
</button>
{% endif %}
</div>
{%endif%}
{%if build.outcome == build.IN_PROGRESS %}
- <div class="span4 offset1">
- <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete">
- <div style="width: {{build.completeper}}%;" class="bar"></div>
- </div>
- </div>
- <div class="lead pull-right">{{build.completeper}}% of tasks complete</div>
- {%endif%}
- </div>
+ <div class="span4" style="display:none" id="cancelling-msg-{{build.buildrequest.pk}}">
+ <p class="lead">Cancelling the build ...</p>
</div>
+ <div class="span4 offset1 progress-info">
+ <div class="progress" id="build-pc-done-title-{{build.pk}}" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete">
+ <div id="build-pc-done-bar-{{build.pk}}" style="width: {{build.completeper}}%;" class="bar"></div>
+ </div>
+ </div>
+ <div class="lead span3 progress-info"><span id="build-pc-done-{{build.pk}}">{{build.completeper}}</span>% of tasks complete</div>
+ {# No build cancel for command line builds project #}
+ {% if build.project.is_default %}
+ <i class="icon-question-sign get-help get-help-blue pull-right" title="" data-original-title="Builds in this project cannot be cancelled from Toaster: they can only be cancalled from the command line"></i>
+ {% else %}
+ <div class="lead pull-right progress-info">
+ <button class="cancel-build-btn btn btn-info pull-right"
+ data-buildrequest-id={{build.buildrequest.pk}}
+ data-request-url="{% url 'xhr_buildrequest' build.project.pk %}" >
+ Cancel
+ </button>
+ </div>
+ {% endif %}
- {% endfor %}
- </div>
-
-<script>
-
-function scheduleBuild(url, projectName, projectUrl, buildlist) {
- console.log("scheduleBuild");
- libtoaster.startABuild(url, null, buildlist.join(" "), function(){
- console.log("reloading page");
- window.location.reload();
- }, null);
-}
-
-$(document).ready(function(){
+ {%endif%} {# end if in progress #}
- $(".cancel-build-btn").click(function (){
- var url = $(this).data('request-url');
- var buildIds = $(this).data('build-id');
- var btn = $(this);
+ {% if build.outcome == build.CANCELLED %}
+ <div class="span4">
+ <p class="lead">Build cancelled</p>
+ </div>
+ <button class="btn btn-info pull-right run-again-btn"
+ data-request-url="{% url 'xhr_buildrequest' build.project.pk %}"
+ data-target='{{build.target_set.all|get_tasks|json}}'>
+ Rebuild
- libtoaster.cancelABuild(url, buildIds, function(){
- btn.parents(".alert").fadeOut();
- }, null);
- });
-});
+ </button>
+ {% endif %}
+ </div>
+</div>
-</script>
+ {% endfor %}
+ </div>
{%endif%}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
index 4487b3ea0..46aed901f 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
@@ -4,51 +4,11 @@
{% load static %}
{% block pagecontent %}
-<script src="{% static 'js/newcustomimage.js' %}"></script>
-<script>
- $(document).ready(function (){
- var ctx = {
- xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}",
- };
-
- try {
- newCustomImagePageInit(ctx);
- } catch (e) {
- document.write("Sorry, An error has occurred loading this page");
- console.warn(e);
- }
- });
-</script>
-
-</script>
-<div class="modal hide fade in" id="new-custom-image-modal" aria-hidden="false">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-hidden="true">Ă—</button>
- <h3>Name your custom image</h3>
- </div>
- <div class="modal-body">
- <div class="row-fluid">
- <span class="help-block span8">Image names must be unique. They should not contain spaces or capital letters, and the only allowed special character is dash (-).<p></p>
- </span></div>
- <div class="control-group controls">
- <input type="text" class="huge span5" placeholder="Type the name, something like 'core-image-myimage'">
- <span class="help-block" style="display:none">Image names cannot contain spaces or capital letters. The only allowed special character is dash (-)</span>
- <span class="help-block" style="display: none">An image with this name already exists. Image names must be unique: try a different one.</span>
- </div>
- </div>
- <div class="modal-footer">
- <a href="#" id="create-new-custom-image-btn" class="btn btn-primary btn-large" data-original-title="" title="">Create custom image</a>
- </div>
-</div>
-
+{% include "newcustomimage_modal.html" %}
{% include "projecttopbar.html" %}
-
{% url table_name project.id as xhr_table_url %}
+<h2>{{title}} (<span class="table-count-{{table_name}}">0</span>)</h2>
{% include "toastertable.html" %}
-
-
{% endblock %}
-
-
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html
new file mode 100644
index 000000000..caeb30235
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage_modal.html
@@ -0,0 +1,48 @@
+{% load static %}
+
+<script src="{% static 'js/newcustomimage_modal.js' %}"></script>
+<script>
+ $(document).ready(function (){
+ try {
+ newCustomImageModalInit();
+ } catch (e) {
+ document.write("Sorry, An error has occurred loading this page");
+ console.warn(e);
+ }
+ });
+</script>
+
+<div class="modal hide fade in" id="new-custom-image-modal" aria-hidden="false">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">Ă—</button>
+ <h3>New custom image</h3>
+ </div>
+
+ <div class="modal-body">
+ <!--
+ this container is visible if there are multiple image recipes which could
+ be used as a basis for the new custom image; radio buttons are added to it
+ via newCustomImageModalSetRecipes() as required
+ -->
+ <div data-role="image-selector" style="display:none;">
+ <h4>Which image do you want to customise?</h4>
+ <div data-role="image-selector-radios"></div>
+ <span class="help-block error" id="invalid-recipe-help" style="display:none"></span>
+ <div class="air"></div>
+ </div>
+
+ <h4>Name your custom image</h4>
+
+ <div class="row-fluid">
+ <span class="help-block span8">Image names must be unique. They should not contain spaces or capital letters, and the only allowed special character is dash (-).<p></p>
+ </span></div>
+ <div class="control-group controls">
+ <input type="text" class="huge" placeholder="Type the custom image name" required>
+ <span class="help-block error" id="invalid-name-help" style="display:none"></span>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <button id="create-new-custom-image-btn" class="btn btn-primary btn-large" data-original-title="" title="" disabled>Create custom image</button>
+ </div>
+</div>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html
index b766aeac9..0aefc5625 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html
@@ -1,16 +1,34 @@
-<button class="btn btn-block btn-danger add-rm-package-btn" id="package-rm-btn-{{data.pk}}" data-directive="remove" data-package="{{data.pk}}" data-package-url="{% url 'xhr_customrecipe_packages' extra.recipe_id data.pk %}" style="
- {% if data.pk not in extra.current_packages %}
- display:none
- {% endif %}
- ">
- <i class="icon-trash no-tooltip"></i>
- Remove package
-</a>
-<button class="btn btn-block add-rm-package-btn" data-directive="add" id="package-add-btn-{{data.pk}}" data-package="{{data.pk}}" data-package-url="{% url 'xhr_customrecipe_packages' extra.recipe_id data.pk %}" style="
- {% if data.pk in extra.current_packages %}
- display:none
- {% endif %}
- ">
-<i class="icon-plus"></i>
- Add package
-</button>
+{# TODO move to snippets dir #}
+{% if data.is_locale_package %}
+<p class="text-center">
+ <span class="muted">Locale package</span>
+ <i class="icon-question-sign get-help hover-help" title=""
+ data-original-title="This package is included in your image
+ based on the locale specified in the IMAGE_LINGUAS variable"
+ style="visibility: hidden;">
+ </i>
+</p>
+
+{% else %}
+
+<div id="package-btn-cell-{{data.pk}}">
+ <div style="display: none; font-size: 11px; line-height: 1.3;" class="tooltip-inner inline-notification"></div>
+ <button class="btn btn-block btn-danger add-rm-package-btn" data-directive="remove" data-id="{{data.pk}}" data-package-url="{% url 'xhr_customrecipe_packages' extra.recipe_id data.pk %}" data-name="{{data.name}}" style="
+ {% if data.pk not in extra.current_packages %}
+ display:none
+ {% endif %}
+ ">
+ <i class="icon-trash no-tooltip"></i>
+ Remove package
+ </button>
+ <button class="btn btn-block add-rm-package-btn" data-directive="add" data-id="{{data.pk}}" data-package-url="{% url 'xhr_customrecipe_packages' extra.recipe_id data.pk %}" data-name="{{data.name}}" style="
+ {% if data.pk in extra.current_packages %}
+ display:none
+ {% endif %}
+ ">
+ <i class="icon-plus"></i>
+ Add package
+ </button>
+</div>
+
+{% endif %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/project.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/project.html
index 4e83981f8..125676881 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/project.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/project.html
@@ -24,7 +24,9 @@
});
</script>
-<div id="change-release-modal" class="modal hide fade in" tabindex="-1" role="dialog" aria-labelledby="change-release-modal" aria-hidden="false">
+<!-- Comment out the ability to change the project release, until we decide what to do this functionality -->
+
+<!--div id="change-release-modal" class="modal hide fade in" tabindex="-1" role="dialog" aria-labelledby="change-release-modal" aria-hidden="false">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
<h3>Changing Yocto Project release to <span class="proposed-release-change-name"></span></h3>
@@ -39,7 +41,7 @@
<button id="change-release-and-rm-layers" data-dismiss="modal" type="submit" class="btn btn-primary">Change release and delete layers</button>
<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
</div>
-</div>
+</div-->
<div class="row-fluid" id="project-page" style="display:none">
@@ -68,7 +70,7 @@
<div class="alert alert-info" style="display:none" id="no-most-built">
<span class="lead">You haven't built any recipes yet</span>
- <p style="margin-top: 10px;"><a href="{% url 'projectsoftwarerecipes' project.id %}">Choose a recipe to build</a></p>
+ <p style="margin-top: 10px;"><a href="{% url 'projectimagerecipes' project.id %}">Choose a recipe to build</a></p>
</div>
<ul class="unstyled configuration-list" id="freq-build-list">
@@ -79,12 +81,19 @@
<div class="well well-transparent">
<h3>Project release</h3>
- <p class="lead"><span id="project-release-title"></span> <i title="" data-original-title="" id="release-change-toggle" class="icon-pencil"></i></p>
+ <p class="lead"><span id="project-release-title"></span>
+
+ <!-- Comment out the ability to change the project release, until we decide what to do with this functionality -->
- <form class="form-inline" id="change-release-form" style="display:none;">
+ <!--i title="" data-original-title="" id="release-change-toggle" class="icon-pencil"></i-->
+ </p>
+
+ <!-- Comment out the ability to change the project release, until we decide what to do with this functionality -->
+
+ <!--form class="form-inline" id="change-release-form" style="display:none;">
<select></select>
<button class="btn" style="margin-left:5px;" id="change-release-btn">Change</button> <a href="#" id="cancel-release-change" class="btn btn-link">Cancel</a>
- </form>
+ </form-->
</div>
</div>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
new file mode 100644
index 000000000..6d7e10bac
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
@@ -0,0 +1,56 @@
+{% extends 'base.html' %}
+
+{% load static %}
+
+{% block extraheadcontent %}
+ <link rel="stylesheet" href="{% static 'css/jquery-ui.min.css' %}" type='text/css'>
+ <link rel="stylesheet" href="{% static 'css/jquery-ui.structure.min.css' %}" type='text/css'>
+ <link rel="stylesheet" href="{% static 'css/jquery-ui.theme.min.css' %}" type='text/css'>
+ <script src="{% static 'js/jquery-ui.min.js' %}">
+ </script>
+{% endblock %}
+
+{% block title %} {{title}} - {{project.name}} - Toaster {% endblock %}
+
+{% block pagecontent %}
+
+ {% include "projecttopbar.html" %}
+
+ <div class="row-fluid">
+ {% with mru=mru mrb_type=mrb_type %}
+ {% include 'mrb_section.html' %}
+ {% endwith %}
+
+ <h2 class="page-header top-air" data-role="page-title"></h2>
+
+ {% url 'projectbuilds' project.id as xhr_table_url %}
+ {% include 'toastertable.html' %}
+ </div>
+
+ <script>
+ $(document).ready(function () {
+ // title
+ var tableElt = $("#{{table_name}}");
+ var titleElt = $("[data-role='page-title']");
+
+ tableElt.on("table-done", function (e, total, tableParams) {
+ var title = "All project builds";
+
+ if (tableParams.search || tableParams.filter) {
+ if (total === 0) {
+ title = "No project builds found";
+ }
+ else if (total > 0) {
+ title = total + " project build" + (total > 1 ? 's' : '') + " found";
+ }
+ }
+
+ titleElt.text(title);
+ });
+
+ // highlight builds tab
+ $("#topbar-builds-tab").addClass("active")
+ });
+ </script>
+
+{% endblock %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds.html
index bb38284aa..3402fc4fe 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds.html
@@ -63,11 +63,12 @@
{% for build in objects %} {# if we have a build, just display it #}
<tr class="data">
<td class="outcome"><a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a>
- {% if build.project %}
- &nbsp; <a href="{% url 'build_artifact' build.id "cookerlog" build.id %}">
- <i class="icon-download-alt" title="" data-original-title="Download build log"></i>
- </a>
- {% endif %}
+ {% if build.cooker_log_path %}
+ &nbsp;
+ <a href="{% url 'build_artifact' build.id "cookerlog" build.id %}">
+ <i class="icon-download-alt" title="Download build log"></i>
+ </a>
+ {% endif %}
</td>
<td class="target">
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectconf.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectconf.html
index 30fd03e32..27a898b65 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectconf.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectconf.html
@@ -31,6 +31,28 @@
</dd>
{% endif %}
+ {% if dl_dir_defined %}
+ <dt>
+ <span class="js-config-var-name js-config-var-managed-name">DL_DIR</span>
+ <i class="icon-question-sign get-help" title="Absolute path to the directory used to store downloads required for your builds. By default, Toaster projects share the same downloads directory.<br /><a href='http://www.yoctoproject.org/docs/2.1/ref-manual/ref-manual.html#var-DL_DIR' target='_blank'>Read more in the manual</a>"></i>
+ </dt>
+ <dd class="lead">
+ <span id="dl_dir"{% if dl_dir %}{%else%} class="muted"{%endif%}>{% if dl_dir %}{{dl_dir}}{%else%}Not set{%endif%}</span>
+ <i class="icon-pencil" id="change-dl_dir-icon"></i>
+ <form id="change-dl_dir-form" style="display:none;">
+ <div class="row-fluid">
+ <span class="help-block span4">To set DL_DIR type the absolute path of the download folder.</span>
+ </div>
+ <div class="input-append" id="validate-dl_dir">
+ <input type="text" class="input-xlarge" id="new-dl_dir" placeholder="Type absolute path of the DL_DIR folder">
+ <button id="apply-change-dl_dir" class="btn" type="button">Save</button>
+ <button id="cancel-change-dl_dir" type="button" class="btn btn-link">Cancel</button>
+ </br><span class="help-block error" id="hintError-dl_dir">A valid directory cannot include spaces or any of these characters: . \ ? % * : | " " < ></span>
+ </div>
+ </form>
+ </dd>
+ {% endif %}
+
{% if fstypes_defined %}
<dt>
<span class="js-config-var-name js-config-var-managed-name">IMAGE_FSTYPES</span>
@@ -108,31 +130,28 @@
</dd>
{% endif %}
- {% if sdk_machine_defined %}
+ {% if sstate_dir_defined %}
<dt>
- <span class="js-config-var-name js-config-var-managed-name">SDKMACHINE</span>
- <i class="icon-question-sign get-help" title="Specifies the architecture (i.e. i686 or x86_64) for which to build SDK and ADT items <br /><a href='http://www.yoctoproject.org/docs/1.6.1/ref-manual/ref-manual.html#var-SDKMACHINE' target='_blank'>Read more in the manual</a>"></i>
+ <span class="js-config-var-name js-config-var-managed-name">SSTATE_DIR</span>
+ <i class="icon-question-sign get-help" title="Absolute path to the directory used to store shared state cache files. These files are reused across the builds, which makes the builds faster. By default, Toaster projects share the same cache directory.<br /><a href='http://www.yoctoproject.org/docs/2.1/ref-manual/ref-manual.html#var-SSTATE_DIR' target='_blank'>Read more in the manual</a>"></i>
</dt>
<dd class="lead">
- <span id="sdkmachine">{{sdk_machine}}</span>
- <i id="change-sdkmachine-icon" class="icon-pencil"></i>
- <form id="change-sdkmachine-form" style="display:none;">
- <label class="radio">
- <input type="radio" name="sdkmachine" value="i686">
- i686
- </label>
- <label class="radio">
- <input type="radio" name="sdkmachine" value="x86_64">
- x86_64
- </label>
- <div style="padding-top:10px;">
- <button id="apply-change-sdkmachine" type="button" class="btn">Save</button>
- <button id="cancel-change-sdkmachine" type="button" class="btn btn-link">Cancel</button>
+ <span id="sstate_dir"{% if sstate_dir %}{%else%} class="muted"{%endif%}>{% if sstate_dir %}{{sstate_dir}}{%else%}Not set{%endif%}</span>
+ <i class="icon-pencil" id="change-sstate_dir-icon"></i>
+ <form id="change-sstate_dir-form" style="display:none;">
+ <div class="row-fluid">
+ <span class="help-block span4">To set SSTATE_DIR type the absolute path of the download folder.</span>
+ </div>
+ <div class="input-append" id="validate-sstate_dir">
+ <input type="text" class="input-xlarge" id="new-sstate_dir" placeholder="Type absolute path of the SSTATE_DIR folder">
+ <span class="error">A valid directory name required</span>
+ <button id="apply-change-sstate_dir" class="btn" type="button">Save</button>
+ <button id="cancel-change-sstate_dir" type="button" class="btn btn-link">Cancel</button>
+ </br><p class="help-block error" id="hintError-sstate_dir">A valid directory cannot include spaces or any of these characters: . \ ? % * : | " " < ></span>
</div>
</form>
</dd>
{% endif %}
-
</dl>
<!-- <ul class="unstyled configuration-list" id="configvar-list"> -->
@@ -182,9 +201,7 @@
<code><a href="http://www.yoctoproject.org/docs/1.6.1/ref-manual/ref-manual.html#var-BB_NUMBER_THREADS" target="_blank">BB_NUMBER_THREADS</a></code>
<code>CVS_PROXY_HOST</code>
<code>CVS_PROXY_PORT</code>
- <code><a href="http://www.yoctoproject.org/docs/1.6.1/ref-manual/ref-manual.html#var-DL_DIR" target="_blank">DL_DIR</a></code>
<code><a href="http://www.yoctoproject.org/docs/1.6.1/ref-manual/ref-manual.html#var-PARALLEL_MAKE" target="_blank">PARALLEL_MAKE</a></code>
- <code><a href="http://www.yoctoproject.org/docs/1.6.1/ref-manual/ref-manual.html#var-SSTATE_DIR" target="_blank">SSTATE_DIR</a></code>
<code><a href="http://www.yoctoproject.org/docs/1.6.1/ref-manual/ref-manual.html#var-SSTATE_MIRRORS" target="_blank">SSTATE_MIRRORS</a></code>
<code><a href="http://www.yoctoproject.org/docs/1.6.1/ref-manual/ref-manual.html#var-TMPDIR" target="_blank">TMPDIR</a></code></p>
<p>Plus the following standard shell environment variables:</p>
@@ -527,6 +544,61 @@
});
{% endif %}
+ {% if dl_dir_defined %}
+
+ // change DL_DIR variable
+ $('#change-dl_dir-icon').click(function() {
+ $('#hintError-dl_dir').hide();
+ // preset the edit value
+ var current_val = $("span#dl_dir").text().trim();
+ if (current_val == "Not set") {
+ current_val="";
+ $("#apply-change-dl_dir").attr("disabled","disabled");
+ }
+ $("input#new-dl_dir").val(current_val);
+
+ $('#change-dl_dir-icon, #dl_dir').hide();
+ $("#change-dl_dir-form").slideDown();
+ });
+
+ $('#cancel-change-dl_dir').click(function(){
+ $("#change-dl_dir-form").slideUp(function() {
+ $('#dl_dir, #change-dl_dir-icon').show();
+ });
+ });
+
+ $("#new-dl_dir").on('input', function(){
+ if ($(this).val().trim().length == 0) {
+ $("#apply-change-dl_dir").attr("disabled","disabled");
+ }
+ else {
+ var input = $(this);
+ var re = /^\/([^ <>\\|":\.%\?\*]+)$/;
+ var invalidDir = re.test(input.val());
+ console.log(invalidDir);
+ if ( invalidDir ) {
+ $('#validate-dl_dir').removeClass('control-group error');
+ $("#apply-change-dl_dir").removeAttr("disabled");
+ $('#hintError-dl_dir').hide();
+ } else {
+ $('#validate-dl_dir').addClass('control-group error');
+ $("#apply-change-dl_dir").attr("disabled","disabled");
+ $('#hintError-dl_dir').show();
+ }
+ }
+ });
+
+ $('#apply-change-dl_dir').click(function(){
+ var value = $('#new-dl_dir').val().trim();
+ postEditAjaxRequest({"configvarChange" : 'DL_DIR:'+value});
+ $('#dl_dir').text(value);
+ $('#dl_dir').removeClass('muted');
+ $("#change-dl_dir-form").slideUp(function () {
+ $('#dl_dir, #change-dl_dir-icon').show();
+ });
+ });
+
+ {% endif %}
{% if fstypes_defined %}
// change IMAGE_FSTYPES variable
@@ -774,47 +846,61 @@
});
{% endif %}
+ {% if sstate_dir_defined %}
- {% if sdk_machine_defined %}
- // change SDKMACHINE variable
- $('#change-sdkmachine-icon').click(function() {
- var current_value = document.getElementById("sdkmachine").innerHTML;
- var radios = document.getElementsByName('sdkmachine');
- for (var i = 0, length = radios.length; i < length; i++) {
- radios[i].checked = false;
- if (radios[i].value == current_value) {
- radios[i].checked = true;
- }
+ // change SSTATE_DIR variable
+ $('#change-sstate_dir-icon').click(function() {
+ $('#hintError-sstate_dir').hide();
+ // preset the edit value
+ var current_val = $("span#sstate_dir").text().trim();
+ if (current_val == "Not set") {
+ current_val="";
+ $("#apply-change-sstate_dir").attr("disabled","disabled");
}
- $('#change-sdkmachine-icon, #sdkmachine').hide();
- $("#change-sdkmachine-form").slideDown();
+ $("input#new-sstate_dir").val(current_val);
+
+ $('#change-sstate_dir-icon, #sstate_dir').hide();
+ $("#change-sstate_dir-form").slideDown();
});
- $('#cancel-change-sdkmachine').click(function(){
- $("#change-sdkmachine-form").slideUp(function() {
- $('#sdkmachine, #change-sdkmachine-icon').show();
+ $('#cancel-change-sstate_dir').click(function(){
+ $("#change-sstate_dir-form").slideUp(function() {
+ $('#sstate_dir, #change-sstate_dir-icon').show();
});
});
- $('#apply-change-sdkmachine').click(function(){
- var value="";
- var radios = document.getElementsByName('sdkmachine');
- for (var i = 0, length = radios.length; i < length; i++) {
- if (radios[i].checked) {
- // do whatever you want with the checked radio
- value=radios[i].value;
- break;
+ $("#new-sstate_dir").on('input', function(){
+ if ($(this).val().trim().length == 0) {
+ $("#apply-change-sstate_dir").attr("disabled","disabled");
+ }
+ else {
+ var input = $(this);
+ var re = /^\/([^ <>\\|":\.%\?\*]+)$/;
+ var invalidDir = re.test(input.val());
+ console.log(invalidDir);
+ if ( invalidDir ) {
+ $('#validate-sstate_dir').removeClass('control-group error');
+ $("#apply-change-sstate_dir").removeAttr("disabled");
+ $('#hintError-sstate_dir').hide();
+ } else {
+ $('#validate-sstate_dir').addClass('control-group error');
+ $("#apply-change-sstate_dir").attr("disabled","disabled");
+ $('#hintError-sstate_dir').show();
}
}
- postEditAjaxRequest({"configvarChange" : 'SDKMACHINE:'+value});
- $('#sdkmachine').text(value);
- $("#change-sdkmachine-form").slideUp(function() {
- $('#sdkmachine, #change-sdkmachine-icon').show();
- });
+ });
+ $('#apply-change-sstate_dir').click(function(){
+ var value = $('#new-sstate_dir').val().trim();
+ postEditAjaxRequest({"configvarChange" : 'SSTATE_DIR:'+value});
+ $('#sstate_dir').text(value);
+ $('#sstate_dir').removeClass('muted');
+ $("#change-sstate_dir-form").slideUp(function () {
+ $('#sstate_dir, #change-sstate_dir-icon').show();
+ });
});
- {% endif %}
+ {% endif %}
// add new variable
$("button#add-configvar-button").click( function (evt) {
@@ -830,7 +916,7 @@
$(".save").attr("disabled","disabled");
// Reload page if admin-removed core managed value is manually added back in
- if (0 <= " DISTRO IMAGE_FSTYPES IMAGE_INSTALL_append PACKAGE_CLASSES SDKMACHINE ".indexOf( " "+variable+" " )) {
+ if (0 <= " DISTRO DL_DIR IMAGE_FSTYPES IMAGE_INSTALL_append PACKAGE_CLASSES SSTATE_DIR ".indexOf( " "+variable+" " )) {
// delayed reload to avoid race condition with postEditAjaxRequest
do_reload=true;
}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projects-toastertable.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projects-toastertable.html
new file mode 100644
index 000000000..5814f32d0
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projects-toastertable.html
@@ -0,0 +1,36 @@
+{% extends 'base.html' %}
+
+{% block title %} All projects - Toaster {% endblock %}
+
+{% block pagecontent %}
+
+ <div class="page-header top-air">
+ <h1 data-role="page-title"></h1>
+ </div>
+
+ {% url 'projects' as xhr_table_url %}
+ {% include 'toastertable.html' %}
+
+ <script>
+ $(document).ready(function () {
+ var tableElt = $("#{{table_name}}");
+ var titleElt = $("[data-role='page-title']");
+
+ tableElt.on("table-done", function (e, total, tableParams) {
+ var title = "All projects";
+
+ if (tableParams.search || tableParams.filter) {
+ if (total === 0) {
+ title = "No projects found";
+ }
+ else if (total > 0) {
+ title = total + " project" + (total > 1 ? 's' : '') + " found";
+ }
+ }
+
+ titleElt.text(title);
+ });
+ });
+ </script>
+
+{% endblock %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projects.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projects.html
deleted file mode 100644
index 678a7963b..000000000
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projects.html
+++ /dev/null
@@ -1,92 +0,0 @@
-{% extends "base.html" %}
-
-{% load static %}
-{% load projecttags %}
-{% load project_url_tag %}
-{% load humanize %}
-
-{% block title %} All projects - Toaster {% endblock %}
-
-{% block pagecontent %}
-
-
- <div class="page-header top-air">
- <h1>
- {% if request.GET.filter and objects.paginator.count > 0 or request.GET.search and objects.paginator.count > 0 %}
- {{objects.paginator.count}} project{{objects.paginator.count|pluralize}} found
- {%elif request.GET.filter and objects.paginator.count == 0 or request.GET.search and objects.paginator.count == 0 %}
- No projects found
- {%else%}
- All projects
- {%endif%}
- </h1>
- </div>
-
- {% if objects.paginator.count == 0 %}
- <div class="row-fluid">
- <div class="alert">
- <form class="no-results input-append" id="searchform">
- <input id="search" name="search" class="input-xxlarge" type="text" value="
- {% if request.GET.search %}
- {{request.GET.search}}
- {% endif %}"/>{% if request.GET.search %}<a href="javascript:$('#search').val('');searchform.submit()" class="add-on btn" tabindex="-1"><i class="icon-remove"></i></a>{% endif %}
- <button class="btn" type="submit" value="Search">Search</button>
- <button class="btn btn-link" onclick="javascript:$('#search').val('');searchform.submit()">Show all projects</button>
- </form>
- </div>
- </div>
-
- {% else %} {# We have builds to display #}
- {% include "basetable_top.html" %}
- {% for o in objects %}
- <tr class="data" data-project="{{ o.id }}">
- <td data-project-field="name">
- <a href="{% project_url o %}">{{o.name}}</a>
- </td>
- <td class="updated"><a href="{% project_url o %}">{{o.updated|date:"d/m/y H:i"}}</a></td>
- <td data-project-field="release">
- {% if o.release %}
- <a href="{% url 'project' o.id %}#project-details">{{o.release.name}}</a>
- {% elif o.is_default %}
- <span class="muted">Not applicable</span>
- <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project does not have a release set. It simply collects information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i>
- {% else %}
- No release available
- {% endif %}
- </td>
- <td data-project-field="machine">
- {% if o.is_default %}
- <span class="muted">Not applicable</span>
- <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project does not have a machine set. It simply collects information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i>
- {% else %}
- <a href="{% url 'project' o.id %}#machine-distro">{{o.get_current_machine_name}}</a>
- {% endif %}
- </td>
- {% if o.get_number_of_builds == 0 %}
- <td class="muted">{{o.get_number_of_builds}}</td>
- <td class="loutcome"></td>
- <td class="ltarget"></td>
- <td class="lerrors"></td>
- <td class="lwarnings"></td>
- <td class="limagefiles"></td>
- {% else %}
- <td><a href="{% url 'projectbuilds' o.id %}">{{o.get_number_of_builds}}</a></td>
- <td class="loutcome"><a href="{% url "builddashboard" o.get_last_build_id %}">{%if o.get_last_outcome == build_SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif o.get_last_outcome == build_FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a></td>
- <td class="ltarget"><a href="{% url "builddashboard" o.get_last_build_id %}">{{o.get_last_target}} </a></td>
- <td class="lerrors">{% if o.get_last_errors %}<a class="errors.count error" href="{% url "builddashboard" o.get_last_build_id %}#errors">{{o.get_last_errors}} error{{o.get_last_errors|pluralize}}</a>{%endif%}</td>
- <td class="lwarnings">{% if o.get_last_warnings %}<a class="warnings.count warning" href="{% url "builddashboard" o.get_last_build_id %}#warnings">{{o.get_last_warnings}} warning{{o.get_last_warnings|pluralize}}</a>{%endif%}</td>
- <td class="limagefiles">
- {% if o.get_last_outcome == build_SUCCEEDED %}
- <a href="{%url "builddashboard" o.get_last_build_id %}#images">{{fstypes|get_dict_value:o.id}}</a>
- {% endif %}
- </td>
-
- {% endif %}
- </tr>
- {% endfor %}
- {% include "basetable_bottom.html" %}
- {% endif %} {# empty #}
-
-{% endblock %}
-
-
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projecttopbar.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projecttopbar.html
index ee86b5481..007de06ff 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projecttopbar.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projecttopbar.html
@@ -1,6 +1,24 @@
+{% load static %}
+<script src="{% static 'js/projecttopbar.js' %}"></script>
+<script>
+ $(document).ready(function () {
+ var ctx = {
+ numProjectLayers : {{project.get_project_layer_versions.count}},
+ machine : "{{project.get_current_machine_name|default_if_none:""}}",
+ }
+
+ try {
+ projectTopBarInit(ctx);
+ } catch (e) {
+ document.write("Sorry, An error has occurred loading this page");
+ console.warn(e);
+ }
+ });
+</script>
+
<div class="alert alert-success lead" id="project-created-notification" style="margin-top:15px; display:none">
<button type="button" class="close" data-dismiss="alert">Ă—</button>
- Your project <strong>{{project.name}}</strong> has been created. You can now <a href="{% url 'projectmachines' project.id %}">select your target machine</a> and <a href="{% url 'projectsoftwarerecipes' project.id %}">choose image recipes</a> to build.
+ Your project <strong>{{project.name}}</strong> has been created. You can now <a href="{% url 'projectmachines' project.id %}">select your target machine</a> and <a href="{% url 'projectimagerecipes' project.id %}">choose image recipes</a> to build.
</div>
<!-- project name -->
@@ -26,31 +44,29 @@
{% if not project.is_default %}
<div id="project-topbar">
<ul class="nav nav-pills">
- <li>
- <a href="{% url 'projectbuilds' project.id %}">
- Builds (<span class="total-builds">0</span>)
- </a>
- </li>
<li id="topbar-configuration-tab">
<a href="{% url 'project' project.id %}">
Configuration
</a>
</li>
<li>
+ <a href="{% url 'projectbuilds' project.id %}">
+ Builds ({{project.get_number_of_builds}})
+ </a>
+ </li>
+ <li>
<a href="{% url 'importlayer' project.id %}">
Import layer
</a>
</li>
- {% if CUSTOM_IMAGE %}
<li>
<a href="{% url 'newcustomimage' project.id %}">
New custom image
</a>
</li>
- {% endif %}
<li class="pull-right">
<form class="form-inline" style="margin-bottom:0px;">
- <i class="icon-question-sign get-help heading-help" data-placement="left" title="" data-original-title="Type the name of one or more recipes you want to build, separated by a space. You can also specify a task by appending a semicolon and a task name to the recipe name, like so: <code>busybox:clean</code>"></i>
+ <i class="icon-question-sign get-help heading-help" data-placement="left" title="" data-original-title="Type the name of one or more recipes you want to build, separated by a space. You can also specify a task by appending a colon and a task name to the recipe name, like so: <code>busybox:clean</code>"></i>
<div class="input-append">
<input id="build-input" type="text" class="input-xlarge input-lg build-target-input" placeholder="Type the recipe you want to build" autocomplete="off" disabled>
<button id="build-button" class="btn btn-primary btn-large build-button" data-project-id="{{project.id}}" disabled>Build</button>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe.html
index c6ae2f380..1d6d64e3c 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe.html
@@ -71,6 +71,16 @@
Layer commit
</dt>
<dd class="iscommit">{{layer_version.commit}}</dd>
+ {% if object.provides_set.all %}
+ <dt>
+ <i class="icon-question-sign get-help"
+ title="A list of aliases by which a particular recipe can be known. The additional aliases are
+ synonyms for the recipe and can be useful satisfying dependencies of other recipes during
+ the build"></i>
+ PROVIDES
+ </dt>
+ <dd><code>{% for provider in object.provides_set.all %}{{ provider.name }}&nbsp;{% endfor %}</code></dd>
+ {% endif %}
</dl>
<h2 class="details">Tasks</h2>
@@ -162,7 +172,14 @@
{% for rr in object.r_dependencies_recipe.all|dictsort:"depends_on.name" %}
<tr>
- <td><a href="{% url "recipe" build.pk rr.depends_on.pk %}">{{rr.depends_on.name}}</a></td>
+ <td><a href="{% url "recipe" build.pk rr.depends_on.pk %}">{{rr.depends_on.name}}</a>
+ {% if rr.via %}
+ <span class="muted">satisfied via {{rr.via.name}}</span>
+ <i class="icon-question-sign get-help hover-help"
+ title="This dependency is satisfied by the PROVIDES value
+ {{rr.via.name}} in the {{rr.depends_on.name}} recipe"></i>
+ {% endif %}
+ </td>
<td><a href="{% url "recipe" build.pk rr.depends_on.pk %}">{{rr.depends_on.version}}</a></td>
</tr>
{% endfor %}
@@ -194,7 +211,14 @@
{% for rr in object.r_dependencies_depends.all|dictsort:"recipe.name" %}
<tr>
- <td><a href="{% url "recipe" build.pk rr.recipe.pk %}">{{rr.recipe.name}}</a></td>
+ <td><a href="{% url "recipe" build.pk rr.recipe.pk %}">{{rr.recipe.name}}</a>
+ {% if rr.via %}
+ <span class="muted"> satisfied via {{rr.via.name}}</span>
+ <i class="icon-question-sign get-help hover-help"
+ title="This dependency is satisfied by the PROVIDES value
+ {{rr.via.name}} in the {{rr.depends_on.name}} recipe"></i>
+ {% endif %}
+ </td>
<td><a href="{% url "recipe" build.pk rr.recipe.pk %}">{{rr.recipe.version}}</a></td>
</tr>
{% endfor %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipedetails.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipedetails.html
new file mode 100644
index 000000000..23aa171ce
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipedetails.html
@@ -0,0 +1,177 @@
+{% extends "base.html" %}
+{% load projecttags %}
+{% load humanize %}
+{% load static %}
+{% block pagecontent %}
+
+<div class="section">
+ <ul class="breadcrumb">
+ <li>
+ <a href="{% url 'project' project.id %}">{{project.name}}</a>
+ <span class="divider">&rarr;</span>
+ </li>
+ <li>
+ {% if recipe.is_image %}
+ <a href="{% url 'projectimagerecipes' project.id %}">Image recipes</a>
+ {% else %}
+ <a href="{% url 'projectsoftwarerecipes' project.id %}">Software recipes</a>
+ {% endif %}
+ <span class="divider">&rarr;</span>
+ </li>
+ <li class="active">
+ {{recipe.name}} ({{recipe.layer_version.layer.name}})
+ </li>
+ </ul>
+</div>
+
+<script src="{% static 'js/recipedetails.js' %}"></script>
+<script>
+ $(document).ready(function (){
+ var ctx = {
+ recipe : {
+ id: {{recipe.pk}},
+ name: "{{recipe.name}}",
+ totalPackages: {{packages.count}},
+ layer_version : {
+ id: {{recipe.layer_version.pk}},
+ name: "{{recipe.layer_version.layer.name}}",
+ layerdetailurl: "{% url 'layerdetails' project.pk recipe.layer_version.pk %}"
+ }
+ }
+ };
+
+ try {
+ recipeDetailsPageInit(ctx);
+ } catch (e) {
+ document.write("Sorry, An error has occurred loading this page");
+ console.warn(e);
+ }
+ });
+</script>
+
+{% include 'newcustomimage_modal.html' %}
+
+<div class="row-fluid span11">
+ <div class="alert alert-success lead" id="image-created-notification" style="margin-top: 15px; display: none">
+ <button type="button" data-dismiss="alert" class="close">x</button>
+ Your custom image <strong>{{recipe.name}}</strong> has been created. You can now add or remove packages as needed.
+ </div>
+ <div class="page-header air">
+ <h1>
+ {{recipe.name}}
+ <small>({{recipe.layer_version.layer.name}})</small>
+ </h1>
+ </div>
+</div>
+
+<div class="row-fluid span11">
+ <div class="span8">
+ <div class="button-place btn-group" id="customise-build-btns"
+ style="width: 100%;
+ {% if not in_project %}
+ display:none;
+ {% endif %}">
+ <button class="btn btn-large span6 build-recipe-btn" style="width: 50%">
+ Build {{recipe.name}}
+ </button>
+ {% if recipe.is_image %}
+ <button class="btn btn-large span6 customise-btn" data-recipe="{{recipe.pk}}" style="width: 50%">
+ Customise {{recipe.name}}
+ </button>
+ {% endif %}
+ </div>
+ <div class="button-place">
+ <button class="btn btn-block btn-large" id="add-layer-btn"
+ style="width:100%;
+ {% if in_project %}
+ display:none;
+ {% endif %}">
+ <i class="icon-plus"></i>
+ Add the {{recipe.layer_version.layer.name}} layer to your project to build or customise this image recipe
+ </button>
+ </div>
+
+ <div id="packages-table">
+ {% if packages.count %}
+ {% url 'recipepackages' project.id recipe.id as xhr_table_url %}
+ <h2>{{title}} (<span class="table-count-{{table_name}}">0</span>) </h2>
+ {% include "toastertable.html" %}
+ {% else %}
+ <h2>{{title}}</h2>
+ {% endif %}
+
+ <div class="alert alert-info air" id="build-to-get-packages-msg"
+ {# if there are packages and it's in the project don't show this msg #}
+ {% if packages.count or not packages.count and not in_project %}
+ style="display:none"
+ {% endif %} >
+ <p class="lead">Toaster has no package information for {{recipe.name}}. To generate package information, build {{recipe.name}}</p>
+ <button class="btn btn-info btn-large build-recipe-btn" style="margin:20px 0 10px 0;">Build {{recipe.name}}</button>
+ </div>
+
+ <div class="alert alert-info air" id="packages-alert"
+ {% if packages.count or in_project %}
+ style="display:none"
+ {% endif %}
+ >
+ <p class="lead">Toaster has no package information for {{recipe.name}}
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="span4 well">
+ <h2 style="margin-bottom:20px;">About {{recipe.name}}</h2>
+ <dl>
+ <dt>
+ Approx. packages included
+ <i class="icon-question-sign get-help" title="" data-original-title="The number of packages included is based on information from previous builds and from parsing layers, so we can never be sure it is 100% accurate"></i>
+ </dt>
+ <dd class="no-packages">{{packages.count}}</dd>
+ <dt>
+ Approx. package size
+ <i class="icon-question-sign get-help" title="" data-original-title="Package size is based on information from previous builds, so we can never be sure it is 100% accurate"></i>
+ </dt>
+ <dd>{{approx_pkg_size.size__sum|filtered_filesizeformat}}</dd>
+ {% if last_build %}
+ <dt>Last build</dt>
+ <dd>
+ <i class="icon-ok-sign success"></i>
+ <a href="{% url 'projectbuilds' project.id%}">{{last_build.completed_on|date:"d/m/y H:i"}}</a>
+ </dd>
+ {% endif %}
+ <dt>Recipe file</dt>
+ <dd>
+ <code>{{recipe.file_path|cut_path_prefix:recipe.layer_version.local_path}}</code>
+ <a href="{{recipe.get_vcs_recipe_file_link_url}}"><i class="icon-share" title="" data-original-title="View recipe file"></i></a>
+ </dd>
+ <dt>Layer</dt>
+ <dd><a href="{% url 'layerdetails' project.id recipe.layer_version.pk %}">{{recipe.layer_version.layer.name}}</a></dd>
+ <dt>
+ Summary
+ </dt>
+ <dd>
+ {{recipe.summary}}
+ </dd>
+ <dt>
+ Description
+ </dt>
+ <dd>
+ {{recipe.description}}
+ </dd>
+ <dt>Version</dt>
+ <dd>
+ {{recipe.version}}
+ </dd>
+ <dt>Section</dt>
+ <dd>
+ {{recipe.section}}
+ </dd>
+ <dt>License</dt>
+ <dd>
+ {{recipe.license}}
+ </dd>
+ </dl>
+ </div>
+</div>
+
+{% endblock %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/snippets/pkg_dependencies_popover.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/snippets/pkg_dependencies_popover.html
new file mode 100644
index 000000000..a3fcdb09e
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/snippets/pkg_dependencies_popover.html
@@ -0,0 +1,14 @@
+{# Popover that displays the dependences and sizes of a package 'data' used in the Packages table #}
+{% with data.package_dependencies_source.all_depends.count as dep_count %}
+{% load projecttags %}
+{% if dep_count %}
+ <a data-content="<ul class='unstyled'>
+ {% for dep in data.package_dependencies_source.all_depends %}
+ <li>{{dep.depends_on.name}} {% if dep.depends_on.size > 0 %}({{dep.depends_on.size|filtered_filesizeformat}}){% endif %}</li>
+ {% endfor %}
+ </ul>" title="" class="btn" data-original-title="
+ <strong>{{data.name}}</strong> dependencies - <strong>{{data.package_dependencies_source.get_total_source_deps_size.depends_on__size__sum|filtered_filesizeformat}}</strong>">
+ {{dep_count}}
+</a>
+{% endif %}
+{% endwith %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/snippets/pkg_revdependencies_popover.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/snippets/pkg_revdependencies_popover.html
new file mode 100644
index 000000000..453a9d013
--- /dev/null
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/snippets/pkg_revdependencies_popover.html
@@ -0,0 +1,14 @@
+{# Popover that displays the reverse dependencies and sizes of a package 'data' used in the Packages table #}
+{% with data.package_dependencies_target.all_depends.count as dep_count %}
+{% load projecttags %}
+{% if dep_count %}
+ <a data-content="<ul class='unstyled'>
+ {% for dep in data.package_dependencies_target.all_depends|dictsort:'package.name' %}
+ <li>{{dep.package.name}} {% if dep.package.size > 0 %}({{dep.package.size|filtered_filesizeformat}}){% endif %}</li>
+ {% endfor %}
+ </ul>" title="" class="btn" data-original-title="
+ <strong>{{data.name}}</strong> reverse dependencies - <strong>{{data.package_dependencies_target.get_total_revdeps_size.package_id__size__sum|filtered_filesizeformat}}</strong>">
+ {{dep_count}}
+</a>
+{% endif %}
+{% endwith %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/task.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/task.html
index ef628d9f9..576826243 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/task.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/task.html
@@ -238,7 +238,7 @@
</dl>
{# Performance section - shown only for executed tasks #}
-{%if task.elapsed_time or task.cpu_usage or task.disk_io %}
+{%if task.elapsed_time or task.cpu_time_user or task.cpu_time_system or task.disk_io %}
<h2 class="details">Performance</h2>
{% endif %}
<dl class="dl-horizontal">
@@ -249,19 +249,26 @@
</dt>
<dd>{{task.elapsed_time|format_none_and_zero|floatformat:2}}</dd>
{% endif %}
- {% if task.cpu_usage > 0 %}
+ {% if task.cpu_time_user > 0 %}
<dt>
- <i class="icon-question-sign get-help" title="The percentage of task CPU utilization"></i>
- CPU usage
+ <i class="icon-question-sign get-help" title="Total amount of time spent executing in user mode, in seconds. Note that this time can be greater than the task time due to parallel execution."></i>
+ User CPU time (secs)
</dt>
- <dd>{{task.cpu_usage|format_none_and_zero|floatformat:2}}%</dd>
+ <dd>{{task.cpu_time_user|format_none_and_zero|floatformat:2}}</dd>
+ {% endif %}
+ {% if task.cpu_time_system > 0 %}
+ <dt>
+ <i class="icon-question-sign get-help" title="Total amount of time spent executing in kernel mode, in seconds. Note that this time can be greater than the task time due to parallel execution."></i>
+ System CPU time (secs)
+ </dt>
+ <dd>{{task.cpu_time_system|format_none_and_zero|floatformat:2}}</dd>
{% endif %}
{% if task.disk_io > 0 %}
<dt>
- <i class="icon-question-sign get-help" title="Number of miliseconds the task spent doing disk input and output"></i>
- Disk I/O (ms)
+ <i class="icon-question-sign get-help" title="Number of bytes written to and read from the disk during the task"></i>
+ Disk I/O (bytes)
</dt>
- <dd>{{task.disk_io|format_none_and_zero}}</dd>
+ <dd>{{task.disk_io|format_none_and_zero|intcomma}}</dd>
{% endif %}
</dl>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/tasks.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/tasks.html
index 353410f92..84bc10386 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/tasks.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/tasks.html
@@ -1,4 +1,5 @@
{% extends "basebuildpage.html" %}
+{% load humanize %}
{% load projecttags %}
{% block title %} {{mainheading}} - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster{% endblock %}
@@ -20,13 +21,15 @@
<li><a href="{% url 'buildtime' build.pk %}">Time</a></li>
{% endif %}
{% endblock %}
-{% block nav-cpuusage %}
- {% if 'CPU usage' == mainheading %}
- <li class="active"><a href="{% url 'cpuusage' build.pk %}">CPU usage</a></li>
+
+{% block nav-cputime %}
+ {% if 'CPU time' == mainheading %}
+ <li class="active"><a href="{% url 'cputime' build.pk %}">CPU time</a></li>
{% else %}
- <li><a href="{% url 'cpuusage' build.pk %}">CPU usage</a></li>
+ <li><a href="{% url 'cputime' build.pk %}">CPU time</a></li>
{% endif %}
{% endblock %}
+
{% block nav-diskio %}
{% if 'Disk I/O' == mainheading %}
<li class="active"><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li>
@@ -107,11 +110,14 @@
<td class="time_taken">
{{task.elapsed_time|format_none_and_zero|floatformat:2}}
</td>
- <td class="cpu_used">
- {{task.cpu_usage|format_none_and_zero|floatformat:2}}{% if task.cpu_usage %}%{% endif %}
+ <td class="cpu_time_system">
+ {{task.cpu_time_system|format_none_and_zero|floatformat:2}}
+ </td>
+ <td class="cpu_time_user">
+ {{task.cpu_time_user|format_none_and_zero|floatformat:2}}
</td>
<td class="disk_io">
- {{task.disk_io|format_none_and_zero}}
+ {{task.disk_io|format_none_and_zero|intcomma}}
</td>
</tr>
@@ -125,7 +131,7 @@
<script type="text/javascript">
$(document).ready(function() {
- // enable blue hightlight animation for the order link
+ // highlight heading on the column for the field used for ordering
if (location.href.search('#') > -1) {
var task_order = location.href.split('#')[1];
$("#" + task_order).addClass("highlight");
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable-filter.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable-filter.html
index 7c8dc49b3..4d28793bf 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable-filter.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable-filter.html
@@ -10,7 +10,9 @@
<span id="filter-actions-{{table_name}}"></span>
</div>
<div class="modal-footer">
- <button class="btn btn-primary" type="submit">Apply</button>
+ <button class="btn btn-primary" type="submit" data-role="filter-apply">
+ Apply
+ </button>
</div>
</form>
</div>
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable.html
index 98a715f27..21c3d36c7 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable.html
@@ -32,8 +32,11 @@
<a href="#" class="add-on btn remove-search-btn-{{table_name}}" tabindex="-1">
<i class="icon-remove"></i>
</a>
- <button class="btn search-submit-{{table_name}}" >Search</button>
- <button class="btn btn-link remove-search-btn-{{table_name}}">Show {{title|lower}}
+ <button class="btn search-submit-{{table_name}}">
+ Search
+ </button>
+ <button class="btn btn-link show-all-{{table_name}} remove-search-btn-{{table_name}}">
+ Show all
</button>
</form>
</div>
@@ -43,9 +46,9 @@
<!-- control header -->
<div class="navbar" id="table-chrome-{{table_name}}">
<div class="navbar-inner">
- <div class="navbar-search input-append pull-left">
+ <div class="navbar-search input-append pull-left span6">
- <input class="input-xxlarge" id="search-input-{{table_name}}" name="search" type="text" placeholder="Search {{title|lower}}" value="{%if request.GET.search%}{{request.GET.search}}{%endif%}"/>
+ <input id="search-input-{{table_name}}" name="search" type="text" placeholder="Search {{title|lower}}" value="{%if request.GET.search%}{{request.GET.search}}{%endif%}"/>
<a href="#" style="display:none" class="add-on btn remove-search-btn-{{table_name}}" tabindex="-1">
<i class="icon-remove"></i>
</a>
@@ -54,7 +57,7 @@
<div class="pull-right">
<div class="btn-group">
- <button class="btn dropdown-toggle" data-toggle="dropdown">Edit columns
+ <button id="edit-columns-button" class="btn dropdown-toggle" data-toggle="dropdown">Edit columns
<span class="caret"></span>
</button>
<ul class="dropdown-menu editcol">
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html
index 0301a6c60..2d3d02c2e 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html
@@ -3,15 +3,14 @@
{% load humanize %}
{% load static %}
-{% block title %} Build artifact no longer exists - Toaster {% endblock %}
+{% block title %} Build artifact does not exist - Toaster {% endblock %}
{% block pagecontent %}
-
-<div class="row-fluid air">
- <div class="alert alert-info span8 lead">
- <p"> The build artifact you are trying to download no longer exists.</p>
- <p><a href="javascript:window.history.back()">Back to previous page</a></p>
+ <div class="row-fluid air">
+ <div class="alert alert-info span8 lead">
+ <p>The build artifact you are trying to download does not exist.</p>
+ <p><a href="javascript:window.history.back()">Back to previous page</a></p>
+ </div>
</div>
-</div>
{% endblock %}
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py b/yocto-poky/bitbake/lib/toaster/toastergui/templatetags/objects_to_dictionaries_filter.py
new file mode 100644
index 000000000..0dcc7d271
--- /dev/null
+++ b/yocto-poky/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/yocto-poky/bitbake/lib/toaster/toastergui/tests.py b/yocto-poky/bitbake/lib/toaster/toastergui/tests.py
index 9e6c46a25..a4cab5848 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/tests.py
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/tests.py
@@ -30,90 +30,45 @@ from orm.models import Project, Release, BitbakeVersion, Package, LogMessage
from orm.models import ReleaseLayerSourcePriority, LayerSource, Layer, Build
from orm.models import Layer_Version, Recipe, Machine, ProjectLayer, Target
from orm.models import CustomImageRecipe, ProjectVariable
-from orm.models import Branch
+from orm.models import Branch, CustomImagePackage
import toastermain
+import inspect
+import toastergui
from toastergui.tables import SoftwareRecipesTable
import json
+from datetime import timedelta
from bs4 import BeautifulSoup
import re
+import string
+import json
PROJECT_NAME = "test project"
+PROJECT_NAME2 = "test project 2"
CLI_BUILDS_PROJECT_NAME = 'Command line builds'
-# by default, tests are run in build mode; to run in analysis mode,
-# set this to False in individual test cases
-toastermain.settings.BUILD_MODE = True
-
class ViewTests(TestCase):
"""Tests to verify view APIs."""
- def setUp(self):
- bbv = BitbakeVersion.objects.create(name="test bbv", giturl="/tmp/",
- branch="master", dirpath="")
- release = Release.objects.create(name="test release",
- branch_name="master",
- bitbake_version=bbv)
- self.project = Project.objects.create_project(name=PROJECT_NAME,
- release=release)
- now = timezone.now()
-
- build = Build.objects.create(project=self.project,
- started_on=now,
- completed_on=now)
-
- layersrc = LayerSource.objects.create(sourcetype=LayerSource.TYPE_IMPORTED)
- self.priority = ReleaseLayerSourcePriority.objects.create(release=release,
- layer_source=layersrc)
- layer = Layer.objects.create(name="base-layer", layer_source=layersrc,
- vcs_url="/tmp/")
-
- branch = Branch.objects.create(name="master", layer_source=layersrc)
-
- lver = Layer_Version.objects.create(layer=layer, project=self.project,
- layer_source=layersrc, commit="master",
- up_branch=branch)
-
- self.recipe1 = Recipe.objects.create(layer_source=layersrc,
- name="base-recipe",
- version="1.2",
- summary="one recipe",
- description="recipe",
- layer_version=lver)
-
- Machine.objects.create(layer_version=lver, name="wisk",
- description="wisking machine")
-
- ProjectLayer.objects.create(project=self.project, layercommit=lver)
-
-
- self.customr = CustomImageRecipe.objects.create(\
- name="custom recipe", project=self.project,
- base_recipe=self.recipe1)
-
- self.package = Package.objects.create(name='pkg1', recipe=self.recipe1,
- build=build)
+ fixtures = ['toastergui-unittest-data']
+ def setUp(self):
- # recipe with project for testing AvailableRecipe table
- self.recipe2 = Recipe.objects.create(layer_source=layersrc,
- name="fancy-recipe",
- version="1.4",
- summary="a fancy recipe",
- description="fancy recipe",
- layer_version=lver,
- file_path='/home/foo')
-
- self.assertTrue(lver in self.project.compatible_layerversions())
+ self.project = Project.objects.first()
+ self.recipe1 = Recipe.objects.get(pk=2)
+ self.recipe2 = Recipe.objects.last()
+ self.customr = CustomImageRecipe.objects.first()
+ self.cust_package = CustomImagePackage.objects.first()
+ self.package = Package.objects.first()
+ self.lver = Layer_Version.objects.first()
def test_get_base_call_returns_html(self):
"""Basic test for all-projects view"""
response = self.client.get(reverse('all-projects'), follow=True)
self.assertEqual(response.status_code, 200)
self.assertTrue(response['Content-Type'].startswith('text/html'))
- self.assertTemplateUsed(response, "projects.html")
- self.assertTrue(PROJECT_NAME in response.content)
+ self.assertTemplateUsed(response, "projects-toastertable.html")
def test_get_json_call_returns_json(self):
"""Test for all projects output in json format"""
@@ -128,16 +83,9 @@ class ViewTests(TestCase):
self.assertEqual(data["error"], "ok")
self.assertTrue("rows" in data)
- self.assertTrue(PROJECT_NAME in [x["name"] for x in data["rows"]])
+ self.assertTrue(self.project.name in [x["name"] for x in data["rows"]])
self.assertTrue("id" in data["rows"][0])
- self.assertEqual(sorted(data["rows"][0]),
- ['bitbake_version_id', 'created', 'id',
- 'is_default', 'layersTypeAheadUrl', 'name',
- 'num_builds', 'projectBuildsUrl', 'projectPageUrl',
- 'recipesTypeAheadUrl', 'release_id',
- 'short_description', 'updated', 'user_id'])
-
def test_typeaheads(self):
"""Test typeahead ReST API"""
layers_url = reverse('xhr_layerstypeahead', args=(self.project.id,))
@@ -181,7 +129,6 @@ class ViewTests(TestCase):
return False
- import string
for url in urls:
results = False
@@ -198,15 +145,17 @@ class ViewTests(TestCase):
def test_xhr_import_layer(self):
"""Test xhr_importlayer API"""
+ LayerSource.objects.create(sourcetype=LayerSource.TYPE_IMPORTED)
#Test for importing an already existing layer
args = {'vcs_url' : "git://git.example.com/test",
'name' : "base-layer",
'git_ref': "c12b9596afd236116b25ce26dbe0d793de9dc7ce",
- 'project_id': 1, 'dir_path' : "/path/in/repository"}
+ 'project_id': self.project.id,
+ 'dir_path' : "/path/in/repository"}
response = self.client.post(reverse('xhr_importlayer'), args)
data = json.loads(response.content)
self.assertEqual(response.status_code, 200)
- self.assertNotEqual(data["error"], "ok")
+ self.assertEqual(data["error"], "ok")
#Test to verify import of a layer successful
args['name'] = "meta-oe"
@@ -272,13 +221,12 @@ class ViewTests(TestCase):
def test_xhr_custom_details(self):
"""Test getting custom recipe details"""
- name = "custom recipe"
url = reverse('xhr_customrecipe_id', args=(self.customr.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = {"error": "ok",
"info": {'id': self.customr.id,
- 'name': name,
+ 'name': self.customr.name,
'base_recipe_id': self.recipe1.id,
'project_id': self.project.id,
}
@@ -290,7 +238,9 @@ class ViewTests(TestCase):
name = "to be deleted"
recipe = CustomImageRecipe.objects.create(\
name=name, project=self.project,
- base_recipe=self.recipe1)
+ base_recipe=self.recipe1,
+ file_path="/tmp/testing",
+ layer_version=self.customr.layer_version)
url = reverse('xhr_customrecipe_id', args=(recipe.id,))
response = self.client.delete(url)
self.assertEqual(response.status_code, 200)
@@ -303,20 +253,34 @@ class ViewTests(TestCase):
def test_xhr_custom_packages(self):
"""Test adding and deleting package to a custom recipe"""
- url = reverse('xhr_customrecipe_packages',
- args=(self.customr.id, self.package.id))
- # add self.package1 to recipe
- response = self.client.put(url)
+ # add self.package to recipe
+ response = self.client.put(reverse('xhr_customrecipe_packages',
+ args=(self.customr.id,
+ self.cust_package.id)))
+
self.assertEqual(response.status_code, 200)
- self.assertEqual(json.loads(response.content), {"error": "ok"})
- self.assertEqual(self.customr.packages.all()[0].id, self.package.id)
+ self.assertEqual(json.loads(response.content),
+ {"error": "ok"})
+ self.assertEqual(self.customr.appends_set.first().name,
+ self.cust_package.name)
# delete it
- response = self.client.delete(url)
+ to_delete = self.customr.appends_set.first().pk
+ del_url = reverse('xhr_customrecipe_packages',
+ args=(self.customr.id, to_delete))
+
+ response = self.client.delete(del_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content), {"error": "ok"})
- self.assertFalse(self.customr.packages.all())
- # delete it again to test error condition
- response = self.client.delete(url)
+ all_packages = self.customr.get_all_packages().values_list('pk',
+ flat=True)
+
+ self.assertFalse(to_delete in all_packages)
+ # delete invalid package to test error condition
+ del_url = reverse('xhr_customrecipe_packages',
+ args=(self.customr.id,
+ 99999))
+
+ response = self.client.delete(del_url)
self.assertEqual(response.status_code, 200)
self.assertNotEqual(json.loads(response.content)["error"], "ok")
@@ -332,6 +296,18 @@ class ViewTests(TestCase):
self.assertNotEqual(json.loads(response.content),
{"error": "ok"})
+ def test_download_custom_recipe(self):
+ """Download the recipe file generated for the custom image"""
+
+ # Create a dummy recipe file for the custom image generation to read
+ open("/tmp/a_recipe.bb", 'wa').close()
+ response = self.client.get(reverse('customrecipedownload',
+ args=(self.project.id,
+ self.customr.id)))
+
+ self.assertEqual(response.status_code, 200)
+
+
def test_software_recipes_table(self):
"""Test structure returned for Software RecipesTable"""
table = SoftwareRecipesTable()
@@ -344,7 +320,6 @@ class ViewTests(TestCase):
row2 = next(x for x in rows if x['name'] == self.recipe2.name)
self.assertEqual(response.status_code, 200, 'should be 200 OK status')
- self.assertEqual(len(rows), 2, 'should be 2 recipes')
# check other columns have been populated correctly
self.assertEqual(row1['name'], self.recipe1.name)
@@ -360,568 +335,160 @@ class ViewTests(TestCase):
self.assertEqual(row2['layer_version__layer__name'],
self.recipe2.layer_version.layer.name)
-class LandingPageTests(TestCase):
- """ Tests for redirects on the landing page """
- # disable bogus pylint message error:
- # "Instance of 'WSGIRequest' has no 'url' member (no-member)"
- # (see https://github.com/landscapeio/pylint-django/issues/42)
- # pylint: disable=E1103
-
- LANDING_PAGE_TITLE = 'This is Toaster'
-
- def setUp(self):
- """ Add default project manually """
- self.project = Project.objects.create_project('foo', None)
- self.project.is_default = True
- self.project.save()
-
- def test_only_default_project(self):
- """
- No projects except default
- => get the landing page
- """
- response = self.client.get(reverse('landing'))
- self.assertTrue(self.LANDING_PAGE_TITLE in response.content)
-
- def test_default_project_has_build(self):
- """
- Default project has a build, no other projects
- => get the builds page
- """
- now = timezone.now()
- build = Build.objects.create(project=self.project,
- started_on=now,
- completed_on=now)
- build.save()
-
- response = self.client.get(reverse('landing'))
- self.assertEqual(response.status_code, 302,
- 'response should be a redirect')
- self.assertTrue('/builds' in response.url,
- 'should redirect to builds')
-
- def test_user_project_exists(self):
- """
- User has added a project (without builds)
- => get the projects page
- """
- user_project = Project.objects.create_project('foo', None)
- user_project.save()
-
- response = self.client.get(reverse('landing'))
- self.assertEqual(response.status_code, 302,
- 'response should be a redirect')
- self.assertTrue('/projects' in response.url,
- 'should redirect to projects')
-
- def test_user_project_has_build(self):
- """
- User has added a project (with builds)
- => get the builds page
- """
- user_project = Project.objects.create_project('foo', None)
- user_project.save()
-
- now = timezone.now()
- build = Build.objects.create(project=user_project,
- started_on=now,
- completed_on=now)
- build.save()
-
- response = self.client.get(reverse('landing'))
- self.assertEqual(response.status_code, 302,
- 'response should be a redirect')
- self.assertTrue('/builds' in response.url,
- 'should redirect to builds')
-
-class AllProjectsPageTests(TestCase):
- """ Tests for projects page /projects/ """
-
- MACHINE_NAME = 'delorean'
-
- def setUp(self):
- """ Add default project manually """
- project = Project.objects.create_project(CLI_BUILDS_PROJECT_NAME, None)
- self.default_project = project
- self.default_project.is_default = True
- self.default_project.save()
-
- # this project is only set for some of the tests
- self.project = None
-
- self.release = None
-
- def _add_build_to_default_project(self):
- """ Add a build to the default project (not used in all tests) """
- now = timezone.now()
- build = Build.objects.create(project=self.default_project,
- started_on=now,
- completed_on=now)
- build.save()
-
- def _add_non_default_project(self):
- """ Add another project """
- bbv = BitbakeVersion.objects.create(name="test bbv", giturl="/tmp/",
- branch="master", dirpath="")
- self.release = Release.objects.create(name="test release",
- branch_name="master",
- bitbake_version=bbv)
- self.project = Project.objects.create_project(PROJECT_NAME, self.release)
- self.project.is_default = False
- self.project.save()
-
- # fake the MACHINE variable
- project_var = ProjectVariable.objects.create(project=self.project,
- name='MACHINE',
- value=self.MACHINE_NAME)
- project_var.save()
-
- def test_default_project_hidden(self):
- """ The default project should be hidden if it has no builds """
- params = {"count": 10, "orderby": "updated:-", "page": 1}
- response = self.client.get(reverse('all-projects'), params)
-
- self.assertTrue(not('tr class="data"' in response.content),
- 'should be no project rows in the page')
- self.assertTrue(not(CLI_BUILDS_PROJECT_NAME in response.content),
- 'default project "cli builds" should not be in page')
-
- def test_default_project_has_build(self):
- """ The default project should be shown if it has builds """
- self._add_build_to_default_project()
-
- params = {"count": 10, "orderby": "updated:-", "page": 1}
- response = self.client.get(reverse('all-projects'), params)
-
- self.assertTrue('tr class="data"' in response.content,
- 'should be a project row in the page')
- self.assertTrue(CLI_BUILDS_PROJECT_NAME in response.content,
- 'default project "cli builds" should be in page')
-
- def test_default_project_release(self):
- """
- The release for the default project should display as
- 'Not applicable'
- """
- # need a build, otherwise project doesn't display at all
- self._add_build_to_default_project()
-
- # another project to test, which should show release
- self._add_non_default_project()
-
- response = self.client.get(reverse('all-projects'), follow=True)
- soup = BeautifulSoup(response.content)
-
- # check the release cell for the default project
- attrs = {'data-project': str(self.default_project.id)}
- rows = soup.find_all('tr', attrs=attrs)
- self.assertEqual(len(rows), 1, 'should be one row for default project')
- cells = rows[0].find_all('td', attrs={'data-project-field': 'release'})
- self.assertEqual(len(cells), 1, 'should be one release cell')
- text = cells[0].select('span.muted')[0].text
- self.assertEqual(text, 'Not applicable',
- 'release should be not applicable for default project')
-
- # check the link in the release cell for the other project
- attrs = {'data-project': str(self.project.id)}
- rows = soup.find_all('tr', attrs=attrs)
- cells = rows[0].find_all('td', attrs={'data-project-field': 'release'})
- text = cells[0].select('a')[0].text
- self.assertEqual(text, self.release.name,
- 'release name should be shown for non-default project')
-
- def test_default_project_machine(self):
- """
- The machine for the default project should display as
- 'Not applicable'
- """
- # need a build, otherwise project doesn't display at all
- self._add_build_to_default_project()
-
- # another project to test, which should show machine
- self._add_non_default_project()
-
- response = self.client.get(reverse('all-projects'), follow=True)
- soup = BeautifulSoup(response.content)
-
- # check the machine cell for the default project
- attrs = {'data-project': str(self.default_project.id)}
- rows = soup.find_all('tr', attrs=attrs)
- self.assertEqual(len(rows), 1, 'should be one row for default project')
- cells = rows[0].find_all('td', attrs={'data-project-field': 'machine'})
- self.assertEqual(len(cells), 1, 'should be one machine cell')
- text = cells[0].select('span.muted')[0].text
- self.assertEqual(text, 'Not applicable',
- 'machine should be not applicable for default project')
-
- # check the link in the machine cell for the other project
- attrs = {'data-project': str(self.project.id)}
- rows = soup.find_all('tr', attrs=attrs)
- cells = rows[0].find_all('td', attrs={'data-project-field': 'machine'})
- text = cells[0].select('a')[0].text
- self.assertEqual(text, self.MACHINE_NAME,
- 'machine name should be shown for non-default project')
-
- def test_project_page_links(self):
- """
- Test that links for the default project point to the builds
- page /projects/X/builds for that project, and that links for
- other projects point to their configuration pages /projects/X/
- """
-
- # need a build, otherwise project doesn't display at all
- self._add_build_to_default_project()
-
- # another project to test, which should show machine
- self._add_non_default_project()
-
- response = self.client.get(reverse('all-projects'), follow=True)
- soup = BeautifulSoup(response.content)
-
- # link for default project
- row = soup.find('tr', attrs={'data-project': self.default_project.id})
- cell = row.find('td', attrs={'data-project-field': 'name'})
- expected_url = reverse('projectbuilds', args=(self.default_project.id,))
- self.assertEqual(cell.find('a')['href'], expected_url,
- 'link on default project name should point to builds')
-
- # link for other project
- row = soup.find('tr', attrs={'data-project': self.project.id})
- cell = row.find('td', attrs={'data-project-field': 'name'})
- expected_url = reverse('project', args=(self.project.id,))
- self.assertEqual(cell.find('a')['href'], expected_url,
- 'link on project name should point to configuration')
+ def test_toaster_tables(self):
+ """Test all ToasterTables instances"""
+ current_recipes = self.project.get_available_recipes()
+
+ def get_data(table, options={}):
+ """Send a request and parse the json response"""
+ options['format'] = "json"
+ options['nocache'] = "true"
+ request = RequestFactory().get('/', options)
+
+ # This is the image recipe needed for a package list for
+ # PackagesTable do this here to throw a non exist exception
+ image_recipe = Recipe.objects.get(pk=4)
+
+ # Add any kwargs that are needed by any of the possible tables
+ args = {'pid': self.project.id,
+ 'layerid': self.lver.pk,
+ 'recipeid': self.recipe1.pk,
+ 'recipe_id': image_recipe.pk,
+ 'custrecipeid': self.customr.pk
+ }
-class ProjectBuildsPageTests(TestCase):
- """ Test data at /project/X/builds is displayed correctly """
+ response = table.get(request, **args)
+ return json.loads(response.content)
+
+ # Get a list of classes in tables module
+ tables = inspect.getmembers(toastergui.tables, inspect.isclass)
+
+ for name, table_cls in tables:
+ # Filter out the non ToasterTables from the tables module
+ if not issubclass(table_cls, toastergui.widgets.ToasterTable) or \
+ table_cls == toastergui.widgets.ToasterTable:
+ continue
+
+ # Get the table data without any options, this also does the
+ # initialisation of the table i.e. setup_columns,
+ # setup_filters and setup_queryset that we can use later
+ table = table_cls()
+ all_data = get_data(table)
+
+ self.assertTrue(len(all_data['rows']) > 1,
+ "Cannot test on a %s table with < 1 row" % name)
+
+ if table.default_orderby:
+ row_one = all_data['rows'][0][table.default_orderby.strip("-")]
+ row_two = all_data['rows'][1][table.default_orderby.strip("-")]
+
+ if '-' in table.default_orderby:
+ self.assertTrue(row_one >= row_two,
+ "Default ordering not working on %s"
+ " '%s' should be >= '%s'" %
+ (name, row_one, row_two))
+ else:
+ self.assertTrue(row_one <= row_two,
+ "Default ordering not working on %s"
+ " '%s' should be <= '%s'" %
+ (name, row_one, row_two))
+
+ # Test the column ordering and filtering functionality
+ for column in table.columns:
+ if column['orderable']:
+ # If a column is orderable test it in both order
+ # directions ordering on the columns field_name
+ ascending = get_data(table_cls(),
+ {"orderby" : column['field_name']})
+
+ row_one = ascending['rows'][0][column['field_name']]
+ row_two = ascending['rows'][1][column['field_name']]
+
+ self.assertTrue(row_one <= row_two,
+ "Ascending sort applied but row 0 is less "
+ "than row 1 %s %s " %
+ (column['field_name'], name))
+
+
+ descending = get_data(table_cls(),
+ {"orderby" :
+ '-'+column['field_name']})
+
+ row_one = descending['rows'][0][column['field_name']]
+ row_two = descending['rows'][1][column['field_name']]
+
+ self.assertTrue(row_one >= row_two,
+ "Descending sort applied but row 0 is "
+ "greater than row 1 %s %s" %
+ (column['field_name'], name))
+
+ # If the two start rows are the same we haven't actually
+ # changed the order
+ self.assertNotEqual(ascending['rows'][0],
+ descending['rows'][0],
+ "An orderby %s has not changed the "
+ "order of the data in table %s" %
+ (column['field_name'], name))
+
+ if column['filter_name']:
+ # If a filter is available for the column get the filter
+ # info. This contains what filter actions are defined.
+ filter_info = get_data(table_cls(),
+ {"cmd": "filterinfo",
+ "name": column['filter_name']})
+ self.assertTrue(len(filter_info['filter_actions']) > 0,
+ "Filter %s was defined but no actions "
+ "added to it" % column['filter_name'])
+
+ for filter_action in filter_info['filter_actions']:
+ # filter string to pass as the option
+ # This is the name of the filter:action
+ # e.g. project_filter:not_in_project
+ filter_string = "%s:%s" % (column['filter_name'],
+ filter_action['action_name'])
+ # Now get the data with the filter applied
+ filtered_data = get_data(table_cls(),
+ {"filter" : filter_string})
+
+ # date range filter actions can't specify the
+ # number of results they return, so their count is 0
+ if filter_action['count'] != None:
+ self.assertEqual(len(filtered_data['rows']),
+ int(filter_action['count']),
+ "We added a table filter for %s but "
+ "the number of rows returned was not "
+ "what the filter info said there "
+ "would be" % name)
+
+
+ # Test search functionality on the table
+ something_found = False
+ for search in list(string.ascii_letters):
+ search_data = get_data(table_cls(), {'search' : search})
+
+ if len(search_data['rows']) > 0:
+ something_found = True
+ break
- def setUp(self):
- bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/",
- branch="master", dirpath="")
- release = Release.objects.create(name="release1",
- bitbake_version=bbv)
- self.project1 = Project.objects.create_project(name=PROJECT_NAME,
- release=release)
- self.project1.save()
-
- self.project2 = Project.objects.create_project(name=PROJECT_NAME,
- release=release)
- self.project2.save()
-
- self.default_project = Project.objects.create_project(
- name=CLI_BUILDS_PROJECT_NAME,
- release=release
- )
- self.default_project.is_default = True
- self.default_project.save()
-
- # parameters for builds to associate with the projects
- now = timezone.now()
-
- self.project1_build_success = {
- "project": self.project1,
- "started_on": now,
- "completed_on": now,
- "outcome": Build.SUCCEEDED
- }
-
- self.project1_build_in_progress = {
- "project": self.project1,
- "started_on": now,
- "completed_on": now,
- "outcome": Build.IN_PROGRESS
- }
-
- self.project2_build_success = {
- "project": self.project2,
- "started_on": now,
- "completed_on": now,
- "outcome": Build.SUCCEEDED
- }
-
- self.project2_build_in_progress = {
- "project": self.project2,
- "started_on": now,
- "completed_on": now,
- "outcome": Build.IN_PROGRESS
- }
-
- def _get_rows_for_project(self, project_id):
- """ Helper to retrieve HTML rows for a project """
- url = reverse("projectbuilds", args=(project_id,))
- response = self.client.get(url, follow=True)
- soup = BeautifulSoup(response.content)
- return soup.select('tr[class="data"]')
-
- def test_show_builds_for_project(self):
- """ Builds for a project should be displayed """
- Build.objects.create(**self.project1_build_success)
- Build.objects.create(**self.project1_build_success)
- build_rows = self._get_rows_for_project(self.project1.id)
- self.assertEqual(len(build_rows), 2)
-
- def test_show_builds_project_only(self):
- """ Builds for other projects should be excluded """
- Build.objects.create(**self.project1_build_success)
- Build.objects.create(**self.project1_build_success)
- Build.objects.create(**self.project1_build_success)
-
- # shouldn't see these two
- Build.objects.create(**self.project2_build_success)
- Build.objects.create(**self.project2_build_in_progress)
-
- build_rows = self._get_rows_for_project(self.project1.id)
- self.assertEqual(len(build_rows), 3)
-
- def test_builds_exclude_in_progress(self):
- """ "in progress" builds should not be shown """
- Build.objects.create(**self.project1_build_success)
- Build.objects.create(**self.project1_build_success)
-
- # shouldn't see this one
- Build.objects.create(**self.project1_build_in_progress)
-
- # shouldn't see these two either, as they belong to a different project
- Build.objects.create(**self.project2_build_success)
- Build.objects.create(**self.project2_build_in_progress)
-
- build_rows = self._get_rows_for_project(self.project1.id)
- self.assertEqual(len(build_rows), 2)
-
- def test_tasks_in_projectbuilds(self):
- """ Task should be shown as suffix on build name """
- build = Build.objects.create(**self.project1_build_success)
- Target.objects.create(build=build, target='bash', task='clean')
- url = reverse("projectbuilds", args=(self.project1.id,))
- response = self.client.get(url, follow=True)
- result = re.findall('^ +bash:clean$', response.content, re.MULTILINE)
- self.assertEqual(len(result), 2)
-
- def test_cli_builds_hides_tabs(self):
- """
- Display for command line builds should hide tabs;
- note that the latest builds section is already tested in
- AllBuildsPageTests, as the template is the same
- """
- url = reverse("projectbuilds", args=(self.default_project.id,))
- response = self.client.get(url, follow=True)
- soup = BeautifulSoup(response.content)
- tabs = soup.select('#project-topbar')
- self.assertEqual(len(tabs), 0,
- 'should be no top bar shown for command line builds')
-
- def test_non_cli_builds_has_tabs(self):
- """
- Non-command-line builds projects should show the tabs
- """
- url = reverse("projectbuilds", args=(self.project1.id,))
- response = self.client.get(url, follow=True)
- soup = BeautifulSoup(response.content)
- tabs = soup.select('#project-topbar')
- self.assertEqual(len(tabs), 1,
- 'should be a top bar shown for non-command-line builds')
-
-class AllBuildsPageTests(TestCase):
- """ Tests for all builds page /builds/ """
+ self.assertTrue(something_found,
+ "We went through the whole alphabet and nothing"
+ " was found for the search of table %s" % name)
- def setUp(self):
- bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/",
- branch="master", dirpath="")
- release = Release.objects.create(name="release1",
- bitbake_version=bbv)
- self.project1 = Project.objects.create_project(name=PROJECT_NAME,
- release=release)
- self.default_project = Project.objects.create_project(
- name=CLI_BUILDS_PROJECT_NAME,
- release=release
- )
- self.default_project.is_default = True
- self.default_project.save()
-
- # parameters for builds to associate with the projects
- now = timezone.now()
-
- self.project1_build_success = {
- "project": self.project1,
- "started_on": now,
- "completed_on": now,
- "outcome": Build.SUCCEEDED
- }
-
- self.default_project_build_success = {
- "project": self.default_project,
- "started_on": now,
- "completed_on": now,
- "outcome": Build.SUCCEEDED
- }
-
- def test_show_tasks_in_allbuilds(self):
- """ Task should be shown as suffix on build name """
- build = Build.objects.create(**self.project1_build_success)
- Target.objects.create(build=build, target='bash', task='clean')
- url = reverse('all-builds')
- response = self.client.get(url, follow=True)
- result = re.findall('bash:clean', response.content, re.MULTILINE)
- self.assertEqual(len(result), 3)
-
- def test_no_run_again_for_cli_build(self):
- """ "Run again" button should not be shown for command-line builds """
- build = Build.objects.create(**self.default_project_build_success)
- url = reverse('all-builds')
- response = self.client.get(url, follow=True)
- soup = BeautifulSoup(response.content)
-
- attrs = {'data-latest-build-result': build.id}
- result = soup.find('div', attrs=attrs)
-
- # shouldn't see a run again button for command-line builds
- run_again_button = result.select('button')
- self.assertEqual(len(run_again_button), 0)
-
- # should see a help icon for command-line builds
- help_icon = result.select('i.get-help-green')
- self.assertEqual(len(help_icon), 1)
-
- def test_tooltips_on_project_name(self):
- """
- A tooltip should be present next to the command line
- builds project name in the all builds page, but not for
- other projects
- """
- build1 = Build.objects.create(**self.project1_build_success)
- default_build = Build.objects.create(**self.default_project_build_success)
-
- url = reverse('all-builds')
- response = self.client.get(url, follow=True)
- soup = BeautifulSoup(response.content)
-
- # no help icon on non-default project name
- result = soup.find('tr', attrs={'data-table-build-result': build1.id})
- name = result.select('td.project-name')[0]
- icons = name.select('i.get-help')
- self.assertEqual(len(icons), 0,
- 'should not be a help icon for non-cli builds name')
-
- # help icon on default project name
- result = soup.find('tr', attrs={'data-table-build-result': default_build.id})
- name = result.select('td.project-name')[0]
- icons = name.select('i.get-help')
- self.assertEqual(len(icons), 1,
- 'should be a help icon for cli builds name')
-
-class ProjectPageTests(TestCase):
- """ Test project data at /project/X/ is displayed correctly """
- CLI_BUILDS_PROJECT_NAME = 'Command line builds'
-
- def test_command_line_builds_in_progress(self):
- """
- In progress builds should not cause an error to be thrown
- when navigating to "command line builds" project page;
- see https://bugzilla.yoctoproject.org/show_bug.cgi?id=8277
- """
-
- # add the "command line builds" default project; this mirrors what
- # we do in migration 0026_set_default_project.py
- default_project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None)
- default_project.is_default = True
- default_project.save()
-
- # add an "in progress" build for the default project
- now = timezone.now()
- build = Build.objects.create(project=default_project,
- started_on=now,
- completed_on=now,
- outcome=Build.IN_PROGRESS)
-
- # navigate to the project page for the default project
- url = reverse("project", args=(default_project.id,))
- response = self.client.get(url, follow=True)
+ # Test the limit functionality on the table
+ limited_data = get_data(table_cls(), {'limit' : "1"})
+ self.assertEqual(len(limited_data['rows']),
+ 1,
+ "Limit 1 set on table %s but not 1 row returned"
+ % name)
- self.assertEqual(response.status_code, 200)
+ # Test the pagination functionality on the table
+ page_one_data = get_data(table_cls(), {'limit' : "1",
+ "page": "1"})['rows'][0]
-class BuildDashboardTests(TestCase):
- """ Tests for the build dashboard /build/X """
+ page_two_data = get_data(table_cls(), {'limit' : "1",
+ "page": "2"})['rows'][0]
- def setUp(self):
- bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/",
- branch="master", dirpath="")
- release = Release.objects.create(name="release1",
- bitbake_version=bbv)
- project = Project.objects.create_project(name=PROJECT_NAME,
- release=release)
-
- now = timezone.now()
-
- self.build1 = Build.objects.create(project=project,
- started_on=now,
- completed_on=now)
-
- # exception
- msg1 = 'an exception was thrown'
- self.exception_message = LogMessage.objects.create(
- build=self.build1,
- level=LogMessage.EXCEPTION,
- message=msg1
- )
-
- # critical
- msg2 = 'a critical error occurred'
- self.critical_message = LogMessage.objects.create(
- build=self.build1,
- level=LogMessage.CRITICAL,
- message=msg2
- )
-
- def _get_build_dashboard_errors(self):
- """
- Get a list of HTML fragments representing the errors on the
- build dashboard
- """
- url = reverse('builddashboard', args=(self.build1.id,))
- response = self.client.get(url)
- soup = BeautifulSoup(response.content)
- return soup.select('#errors div.alert-error')
-
- def _check_for_log_message(self, log_message):
- """
- Check whether the LogMessage instance <log_message> is
- represented as an HTML error in the build dashboard page
- """
- errors = self._get_build_dashboard_errors()
- self.assertEqual(len(errors), 2)
-
- expected_text = log_message.message
- expected_id = str(log_message.id)
-
- found = False
- for error in errors:
- error_text = error.find('pre').text
- text_matches = (error_text == expected_text)
-
- error_id = error['data-error']
- id_matches = (error_id == expected_id)
-
- if text_matches and id_matches:
- found = True
- break
-
- template_vars = (expected_text, error_text,
- expected_id, error_id)
- assertion_error_msg = 'exception not found as error: ' \
- 'expected text "%s" and got "%s"; ' \
- 'expected ID %s and got %s' % template_vars
- self.assertTrue(found, assertion_error_msg)
-
- def test_exceptions_show_as_errors(self):
- """
- LogMessages with level EXCEPTION should display in the errors
- section of the page
- """
- self._check_for_log_message(self.exception_message)
-
- def test_criticals_show_as_errors(self):
- """
- LogMessages with level CRITICAL should display in the errors
- section of the page
- """
- self._check_for_log_message(self.critical_message)
+ self.assertNotEqual(page_one_data,
+ page_two_data,
+ "Changed page on table %s but first row is the "
+ "same as the previous page" % name)
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/urls.py b/yocto-poky/bitbake/lib/toaster/toastergui/urls.py
index a1adbb7be..27b0baabf 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/urls.py
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/urls.py
@@ -22,12 +22,16 @@ from django.views.generic import RedirectView, TemplateView
from django.http import HttpResponseBadRequest
from toastergui import tables
from toastergui import typeaheads
+from toastergui import api
urlpatterns = patterns('toastergui.views',
# landing page
url(r'^landing/$', 'landing', name='landing'),
- url(r'^builds/$', 'builds', name='all-builds'),
+ url(r'^builds/$',
+ tables.AllBuildsTable.as_view(template_name="builds-toastertable.html"),
+ name='all-builds'),
+
# build info navigation
url(r'^build/(?P<build_id>\d+)$', 'builddashboard', name="builddashboard"),
@@ -61,7 +65,7 @@ urlpatterns = patterns('toastergui.views',
url(r'^build/(?P<build_id>\d+)/configuration$', 'configuration', name='configuration'),
url(r'^build/(?P<build_id>\d+)/configvars$', 'configvars', name='configvars'),
url(r'^build/(?P<build_id>\d+)/buildtime$', 'buildtime', name='buildtime'),
- url(r'^build/(?P<build_id>\d+)/cpuusage$', 'cpuusage', name='cpuusage'),
+ url(r'^build/(?P<build_id>\d+)/cputime$', 'cputime', name='cputime'),
url(r'^build/(?P<build_id>\d+)/diskio$', 'diskio', name='diskio'),
# image information dir
@@ -74,12 +78,15 @@ urlpatterns = patterns('toastergui.views',
# project URLs
url(r'^newproject/$', 'newproject', name='newproject'),
-
- url(r'^projects/$', 'projects', name='all-projects'),
+ url(r'^projects/$',
+ tables.ProjectsTable.as_view(template_name="projects-toastertable.html"),
+ name='all-projects'),
url(r'^project/(?P<pid>\d+)/$', 'project', name='project'),
url(r'^project/(?P<pid>\d+)/configuration$', 'projectconf', name='projectconf'),
- url(r'^project/(?P<pid>\d+)/builds/$', 'projectbuilds', name='projectbuilds'),
+ url(r'^project/(?P<pid>\d+)/builds/$',
+ tables.ProjectBuildsTable.as_view(template_name="projectbuilds-toastertable.html"),
+ name='projectbuilds'),
# the import layer is a project-specific functionality;
url(r'^project/(?P<pid>\d+)/importlayer$', 'importlayer', name='importlayer'),
@@ -103,13 +110,10 @@ urlpatterns = patterns('toastergui.views',
tables.NewCustomImagesTable.as_view(template_name="newcustomimage.html"),
name="newcustomimage"),
-
url(r'^project/(?P<pid>\d+)/layers/$',
tables.LayersTable.as_view(template_name="generic-toastertable-page.html"),
name="projectlayers"),
-
-
url(r'^project/(?P<pid>\d+)/layer/(?P<layerid>\d+)$',
'layerdetails', name='layerdetails'),
@@ -126,15 +130,21 @@ urlpatterns = patterns('toastergui.views',
name=tables.LayerMachinesTable.__name__.lower()),
- url(r'^project/(?P<pid>\d+)/customrecipe/(?P<recipeid>\d+)/selectpackages/$',
- tables.SelectPackagesTable.as_view(template_name="generic-toastertable-page.html"), name="recipeselectpackages"),
+ url(r'^project/(?P<pid>\d+)/customrecipe/(?P<custrecipeid>\d+)/selectpackages/$',
+ tables.SelectPackagesTable.as_view(), name="recipeselectpackages"),
- url(r'^project/(?P<pid>\d+)/customrecipe/(?P<recipe_id>\d+)$',
- 'customrecipe',
+ url(r'^project/(?P<pid>\d+)/customrecipe/(?P<custrecipeid>\d+)$',
+ tables.SelectPackagesTable.as_view(template_name="customrecipe.html"),
name="customrecipe"),
+ url(r'^project/(?P<pid>\d+)/customrecipe/(?P<recipe_id>\d+)/download$',
+ 'customrecipe_download',
+ name="customrecipedownload"),
+ url(r'^project/(?P<pid>\d+)/recipe/(?P<recipe_id>\d+)$',
+ tables.PackagesTable.as_view(template_name="recipedetails.html"),
+ name="recipedetails"),
# typeahead api end points
url(r'^xhr_typeahead/(?P<pid>\d+)/layers$',
@@ -162,11 +172,19 @@ urlpatterns = patterns('toastergui.views',
# image customisation functionality
url(r'^xhr_customrecipe/(?P<recipe_id>\d+)/packages/(?P<package_id>\d+|)$',
'xhr_customrecipe_packages', name='xhr_customrecipe_packages'),
+
+ url(r'^xhr_customrecipe/(?P<recipe_id>\d+)/packages/$',
+ 'xhr_customrecipe_packages', name='xhr_customrecipe_packages'),
+
url(r'^xhr_customrecipe/(?P<recipe_id>\d+)$', 'xhr_customrecipe_id',
name='xhr_customrecipe_id'),
url(r'^xhr_customrecipe/', 'xhr_customrecipe',
name='xhr_customrecipe'),
+ url(r'^xhr_buildrequest/project/(?P<pid>\d+)$',
+ api.XhrBuildRequest.as_view(),
+ name='xhr_buildrequest'),
+
# default redirection
- url(r'^$', RedirectView.as_view( url= 'landing')),
+ url(r'^$', RedirectView.as_view(url='landing', permanent=True)),
)
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/views.py b/yocto-poky/bitbake/lib/toaster/toastergui/views.py
index 0e255f1b8..bd5bf6334 100755
--- a/yocto-poky/bitbake/lib/toaster/toastergui/views.py
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/views.py
@@ -27,15 +27,15 @@ import operator,re
from django.db.models import F, Q, Sum, Count, Max
from django.db import IntegrityError, Error
-from django.shortcuts import render, redirect
+from django.shortcuts import render, redirect, get_object_or_404
from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe, LogMessage, Variable
from orm.models import Task_Dependency, Recipe_Dependency, Package, Package_File, Package_Dependency
-from orm.models import Target_Installed_Package, Target_File, Target_Image_File, BuildArtifact
+from orm.models import Target_Installed_Package, Target_File, Target_Image_File, BuildArtifact, CustomImagePackage
from orm.models import BitbakeVersion, CustomImageRecipe
from bldcontrol import bbcontroller
from django.views.decorators.cache import cache_control
from django.core.urlresolvers import reverse, resolve
-from django.core.exceptions import MultipleObjectsReturned
+from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.utils import timezone
@@ -43,7 +43,9 @@ from django.utils.html import escape
from datetime import timedelta, datetime
from django.utils import formats
from toastergui.templatetags.projecttags import json as jsonfilter
+from decimal import Decimal
import json
+import os
from os.path import dirname
from functools import wraps
import itertools
@@ -73,13 +75,9 @@ class MimeTypeFinder(object):
def landing(request):
# in build mode, we redirect to the command-line builds page
# if there are any builds for the default (cli builds) project
- default_project = Project.objects.get_default_project()
+ default_project = Project.objects.get_or_create_default_project()
default_project_builds = Build.objects.filter(project = default_project)
- if (not toastermain.settings.BUILD_MODE) and default_project_builds.count() > 0:
- args = (default_project.id,)
- return redirect(reverse('projectbuilds', args = args), permanent = False)
-
# we only redirect to projects page if there is a user-generated project
num_builds = Build.objects.all().count()
user_projects = Project.objects.filter(is_default = False)
@@ -95,52 +93,6 @@ def landing(request):
return render(request, 'landing.html', context)
-# returns a list for most recent builds;
-def _get_latest_builds(prj=None):
- queryset = Build.objects.all()
-
- if prj is not None:
- queryset = queryset.filter(project = prj)
-
- if not toastermain.settings.BUILD_MODE:
- queryset = queryset.exclude(project__is_default=False)
-
- return list(itertools.chain(
- queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
- queryset.filter(outcome__lt=Build.IN_PROGRESS).order_by("-started_on")[:3] ))
-
-
-# a JSON-able dict of recent builds; for use in the Project page, xhr_ updates, and other places, as needed
-def _project_recent_build_list(prj):
- data = []
- # take the most recent 3 completed builds, plus any builds in progress
- for x in _get_latest_builds(prj):
- d = {
- "id": x.pk,
- "targets" : map(lambda y: {"target": y.target, "task": y.task }, x.target_set.all()), # TODO: create the task entry in the Target table
- "status": x.get_current_status(),
- "errors": map(lambda y: {"type": y.lineno, "msg": y.message, "tb": y.pathname}, (x.logmessage_set.filter(level__gte=LogMessage.WARNING)|x.logmessage_set.filter(level=LogMessage.EXCEPTION))),
- "updated": x.completed_on.strftime('%s')+"000",
- "command_time": (x.completed_on - x.started_on).total_seconds(),
- "br_page_url": reverse('builddashboard', args=(x.pk,) ),
- "build" : map( lambda y: {"id": y.pk,
- "status": y.get_outcome_display(),
- "completed_on" : y.completed_on.strftime('%s')+"000",
- "build_time" : (y.completed_on - y.started_on).total_seconds(),
- "build_page_url" : reverse('builddashboard', args=(y.pk,)),
- 'build_time_page_url': reverse('buildtime', args=(y.pk,)),
- "errors": y.errors.count(),
- "warnings": y.warnings.count(),
- "completeper": y.completeper() if y.outcome == Build.IN_PROGRESS else "0",
- "eta": y.eta().strftime('%s')+"000" if y.outcome == Build.IN_PROGRESS else "0",
- }, [x]),
- }
- data.append(d)
-
- return data
-
-
-
def objtojson(obj):
from django.db.models.query import QuerySet
from django.db.models import Model
@@ -151,6 +103,8 @@ def objtojson(obj):
return obj.total_seconds()
elif isinstance(obj, QuerySet) or isinstance(obj, set):
return list(obj)
+ elif isinstance(obj, Decimal):
+ return str(obj)
elif type(obj).__name__ == "RelatedManager":
return [x.pk for x in obj.all()]
elif hasattr( obj, '__dict__') and isinstance(obj, Model):
@@ -509,7 +463,7 @@ def builddashboard( request, build_id ):
for t in tgts:
elem = { }
elem[ 'target' ] = t
- if ( t.is_image ):
+ if t.is_image:
hasImages = True
npkg = 0
pkgsz = 0
@@ -527,9 +481,13 @@ def builddashboard( request, build_id ):
if ( ndx < 0 ):
ndx = 0;
f = i.file_name[ ndx + 1: ]
- imageFiles.append({ 'id': i.id, 'path': f, 'size' : i.file_size })
- if ( t.is_image and
- (( len( imageFiles ) <= 0 ) or ( len( t.license_manifest_path ) <= 0 ))):
+ imageFiles.append({
+ 'id': i.id,
+ 'path': f,
+ 'size': i.file_size,
+ 'suffix': i.suffix
+ })
+ if t.is_image and (len(imageFiles) <= 0 or len(t.license_manifest_path) <= 0):
targetHasNoImages = True
elem[ 'imageFiles' ] = imageFiles
elem[ 'targetHasNoImages' ] = targetHasNoImages
@@ -549,6 +507,7 @@ def builddashboard( request, build_id ):
context = {
'build' : build,
+ 'project' : build.project,
'hasImages' : hasImages,
'ntargets' : ntargets,
'targets' : targets,
@@ -705,7 +664,9 @@ def recipe_packages(request, build_id, recipe_id):
def target_common( request, build_id, target_id, variant ):
template = "target.html"
- (pagesize, orderby) = _get_parameters_values(request, 25, 'name:+')
+ default_orderby = 'name:+'
+
+ (pagesize, orderby) = _get_parameters_values(request, 25, default_orderby)
mandatory_parameters = { 'count': pagesize, 'page' : 1, 'orderby': orderby }
retval = _verify_parameters( request.GET, mandatory_parameters )
if retval:
@@ -725,8 +686,6 @@ def target_common( request, build_id, target_id, variant ):
queryset = queryset.select_related("recipe", "recipe__layer_version", "recipe__layer_version__layer")
packages = _build_page_range( Paginator(queryset, pagesize), request.GET.get( 'page', 1 ))
-
-
build = Build.objects.get( pk = build_id )
# bring in package dependencies
@@ -839,11 +798,12 @@ 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' ],
'object_search_display': "packages included",
- 'default_orderby' : orderby,
+ 'default_orderby' : default_orderby,
'tablecols' : [
tc_package,
tc_packageVersion,
@@ -979,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,
@@ -1038,29 +1001,29 @@ def tasks_common(request, build_id, variant, task_anchor):
anchor=task_anchor
# default ordering depends on variant
- if 'buildtime' == variant:
- title_variant='Time'
- object_search_display="time data"
- filter_search_display="tasks"
- (pagesize, orderby) = _get_parameters_values(request, 25, 'elapsed_time:-')
- elif 'diskio' == variant:
- title_variant='Disk I/O'
- object_search_display="disk I/O data"
- filter_search_display="tasks"
- (pagesize, orderby) = _get_parameters_values(request, 25, 'disk_io:-')
- elif 'cpuusage' == variant:
- title_variant='CPU usage'
- object_search_display="CPU usage data"
- filter_search_display="tasks"
- (pagesize, orderby) = _get_parameters_values(request, 25, 'cpu_usage:-')
- else :
- title_variant='Tasks'
- object_search_display="tasks"
- filter_search_display="tasks"
- (pagesize, orderby) = _get_parameters_values(request, 25, 'order:+')
+ default_orderby = None
+ filter_search_display = 'tasks'
+
+ if 'buildtime' == variant:
+ default_orderby = 'elapsed_time:-'
+ title_variant = 'Time'
+ object_search_display = 'time data'
+ elif 'diskio' == variant:
+ default_orderby = 'disk_io:-'
+ title_variant = 'Disk I/O'
+ object_search_display = 'disk I/O data'
+ elif 'cputime' == variant:
+ default_orderby = 'cpu_time_system:-'
+ title_variant='CPU time'
+ object_search_display = 'CPU time data'
+ else:
+ default_orderby = 'order:+'
+ title_variant = 'Tasks'
+ object_search_display = 'tasks'
+ (pagesize, orderby) = _get_parameters_values(request, 25, default_orderby)
- mandatory_parameters = { 'count': pagesize, 'page' : 1, 'orderby': orderby }
+ mandatory_parameters = {'count': pagesize, 'page' : 1, 'orderby': orderby}
template = 'tasks.html'
retval = _verify_parameters( request.GET, mandatory_parameters )
@@ -1204,23 +1167,38 @@ def tasks_common(request, build_id, variant, task_anchor):
del tc_time['clclass']
tc_cache['hidden']='1'
- tc_cpu={
- 'name':'CPU usage',
- 'qhelp':'The percentage of task CPU utilization',
- 'orderfield': _get_toggle_order(request, "cpu_usage", True),
- 'ordericon':_get_toggle_order_icon(request, "cpu_usage"),
- 'orderkey' : 'cpu_usage',
- 'clclass': 'cpu_used', 'hidden' : 1,
+ tc_cpu_time_system={
+ 'name':'System CPU time (secs)',
+ 'qhelp':'Total amount of time spent executing in kernel mode, in ' +
+ 'seconds. Note that this time can be greater than the task ' +
+ 'time due to parallel execution.',
+ 'orderfield': _get_toggle_order(request, "cpu_time_system", True),
+ 'ordericon':_get_toggle_order_icon(request, "cpu_time_system"),
+ 'orderkey' : 'cpu_time_system',
+ 'clclass': 'cpu_time_system', 'hidden' : 1,
}
- if 'cpuusage' == variant:
- tc_cpu['hidden']='0'
- del tc_cpu['clclass']
+ tc_cpu_time_user={
+ 'name':'User CPU time (secs)',
+ 'qhelp':'Total amount of time spent executing in user mode, in seconds. ' +
+ 'Note that this time can be greater than the task time due to ' +
+ 'parallel execution.',
+ 'orderfield': _get_toggle_order(request, "cpu_time_user", True),
+ 'ordericon':_get_toggle_order_icon(request, "cpu_time_user"),
+ 'orderkey' : 'cpu_time_user',
+ 'clclass': 'cpu_time_user', 'hidden' : 1,
+ }
+
+ if 'cputime' == variant:
+ tc_cpu_time_system['hidden']='0'
+ tc_cpu_time_user['hidden']='0'
+ del tc_cpu_time_system['clclass']
+ del tc_cpu_time_user['clclass']
tc_cache['hidden']='1'
tc_diskio={
- 'name':'Disk I/O (ms)',
- 'qhelp':'Number of miliseconds the task spent doing disk input and output',
+ 'name':'Disk I/O (bytes)',
+ 'qhelp':'Number of bytes written to and read from the disk during the task',
'orderfield': _get_toggle_order(request, "disk_io", True),
'ordericon':_get_toggle_order_icon(request, "disk_io"),
'orderkey' : 'disk_io',
@@ -1238,8 +1216,9 @@ 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,
+ 'default_orderby' : default_orderby,
'search_term': search_term,
'total_count': queryset_with_search.count(),
'tablecols':[
@@ -1251,7 +1230,8 @@ def tasks_common(request, build_id, variant, task_anchor):
tc_outcome,
tc_cache,
tc_time,
- tc_cpu,
+ tc_cpu_time_system,
+ tc_cpu_time_user,
tc_diskio,
]}
@@ -1272,9 +1252,8 @@ def buildtime(request, build_id):
def diskio(request, build_id):
return tasks_common(request, build_id, 'diskio', '')
-def cpuusage(request, build_id):
- return tasks_common(request, build_id, 'cpuusage', '')
-
+def cputime(request, build_id):
+ return tasks_common(request, build_id, 'cputime', '')
def recipes(request, build_id):
template = 'recipes.html'
@@ -1284,7 +1263,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))
@@ -1303,11 +1285,10 @@ 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,
+ 'project': build.project,
'objects': recipes,
'default_orderby' : 'name:+',
'recipe_deps' : deps,
@@ -1392,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)
@@ -1432,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:+',
@@ -1506,6 +1492,7 @@ def bpackage(request, build_id):
context = {
'objectname': 'packages built',
'build': build,
+ 'project': build.project,
'objects' : packages,
'default_orderby' : 'name:+',
'tablecols':[
@@ -1580,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)
@@ -1893,11 +1885,6 @@ def managedcontextprocessor(request):
"projects": projects,
"non_cli_projects": projects.exclude(is_default=True),
"DEBUG" : toastermain.settings.DEBUG,
-
- # True if Toaster is in build mode, False otherwise
- "BUILD_MODE": toastermain.settings.BUILD_MODE,
-
- "CUSTOM_IMAGE" : toastermain.settings.CUSTOM_IMAGE,
"TOASTER_BRANCH": toastermain.settings.TOASTER_BRANCH,
"TOASTER_REVISION" : toastermain.settings.TOASTER_REVISION,
}
@@ -1908,6 +1895,7 @@ def managedcontextprocessor(request):
import toastermain.settings
from orm.models import Project, ProjectLayer, ProjectTarget, ProjectVariable
+from bldcontrol.models import BuildEnvironment
# we have a set of functions if we're in managed mode, or
# a default "page not available" simple functions for interactive mode
@@ -1926,263 +1914,6 @@ if True:
''' The exception raised on invalid POST requests '''
pass
- # shows the "all builds" page for managed mode; it displays build requests (at least started!) instead of actual builds
- # WARNING _build_list_helper() may raise a RedirectException, which
- # will set the GET parameters and redirect back to the
- # all-builds or projectbuilds page as appropriate;
- # TODO don't use exceptions to control program flow
- @_template_renderer("builds.html")
- def builds(request):
- # define here what parameters the view needs in the GET portion in order to
- # be able to display something. 'count' and 'page' are mandatory for all views
- # that use paginators.
-
- queryset = Build.objects.all()
-
- # if in analysis mode, exclude builds for all projects except
- # command line builds
- if not toastermain.settings.BUILD_MODE:
- queryset = queryset.exclude(project__is_default=False)
-
- redirect_page = resolve(request.path_info).url_name
-
- context, pagesize, orderby = _build_list_helper(request,
- queryset,
- redirect_page)
- # all builds page as a Project column
- context['tablecols'].append({
- 'name': 'Project',
- 'clclass': 'project_column'
- })
-
- _set_parameters_values(pagesize, orderby, request)
- return context
-
-
- # helper function, to be used on "all builds" and "project builds" pages
- def _build_list_helper(request, queryset_all, redirect_page, pid=None):
- default_orderby = 'completed_on:-'
- (pagesize, orderby) = _get_parameters_values(request, 10, default_orderby)
- mandatory_parameters = { 'count': pagesize, 'page' : 1, 'orderby' : orderby }
- retval = _verify_parameters( request.GET, mandatory_parameters )
- if retval:
- params = {}
- if pid:
- params = {'pid': pid}
- raise RedirectException(redirect_page,
- request.GET,
- mandatory_parameters,
- **params)
-
- # boilerplate code that takes a request for an object type and returns a queryset
- # for that object type. copypasta for all needed table searches
- (filter_string, search_term, ordering_string) = _search_tuple(request, Build)
-
- # post-process any date range filters
- filter_string, daterange_selected = _modify_date_range_filter(filter_string)
-
- # don't show "in progress" builds in "all builds" or "project builds"
- queryset_all = queryset_all.exclude(outcome = Build.IN_PROGRESS)
-
- # append project info
- queryset_all = queryset_all.select_related("project")
-
- # annotate with number of ERROR and EXCEPTION log messages
- queryset_all = queryset_all.annotate(
- errors_no = Count(
- 'logmessage',
- only=Q(logmessage__level=LogMessage.ERROR) |
- Q(logmessage__level=LogMessage.EXCEPTION)
- )
- )
-
- # annotate with number of warnings
- q_warnings = Q(logmessage__level=LogMessage.WARNING)
- queryset_all = queryset_all.annotate(
- warnings_no = Count('logmessage', only=q_warnings)
- )
-
- # add timespent field
- timespent = 'completed_on - started_on'
- queryset_all = queryset_all.extra(select={'timespent': timespent})
-
- queryset_with_search = _get_queryset(Build, queryset_all,
- None, search_term,
- ordering_string, '-completed_on')
-
- queryset = _get_queryset(Build, queryset_all,
- filter_string, search_term,
- ordering_string, '-completed_on')
-
- # retrieve the objects that will be displayed in the table; builds a paginator and gets a page range to display
- build_info = _build_page_range(Paginator(queryset, pagesize), request.GET.get('page', 1))
-
- # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds)
- build_mru = _get_latest_builds()[:3]
-
- # calculate the exact begining of local today and yesterday, append context
- context_date,today_begin,yesterday_begin = _add_daterange_context(queryset_all, request, {'started_on','completed_on'})
-
- # set up list of fstypes for each build
- fstypes_map = {};
- for build in build_info:
- targets = Target.objects.filter( build_id = build.id )
- comma = "";
- extensions = "";
- for t in targets:
- if ( not t.is_image ):
- continue
- tif = Target_Image_File.objects.filter( target_id = t.id )
- for i in tif:
- s=re.sub('.*tar.bz2', 'tar.bz2', i.file_name)
- if s == i.file_name:
- s=re.sub('.*\.', '', i.file_name)
- if None == re.search(s,extensions):
- extensions += comma + s
- comma = ", "
- fstypes_map[build.id]=extensions
-
- # send the data to the template
- context = {
- # specific info for
- 'mru' : build_mru,
- # TODO: common objects for all table views, adapt as needed
- 'objects' : build_info,
- 'objectname' : "builds",
- 'default_orderby' : default_orderby,
- 'fstypes' : fstypes_map,
- 'search_term' : search_term,
- 'total_count' : queryset_with_search.count(),
- 'daterange_selected' : daterange_selected,
- # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
- 'tablecols' : [
- {'name': 'Outcome', # column with a single filter
- 'qhelp' : "The outcome tells you if a build successfully completed or failed", # the help button content
- 'dclass' : "span2", # indication about column width; comes from the design
- 'orderfield': _get_toggle_order(request, "outcome"), # adds ordering by the field value; default ascending unless clicked from ascending into descending
- 'ordericon':_get_toggle_order_icon(request, "outcome"),
- # filter field will set a filter on that column with the specs in the filter description
- # the class field in the filter has no relation with clclass; the control different aspects of the UI
- # still, it is recommended for the values to be identical for easy tracking in the generated HTML
- 'filter' : {'class' : 'outcome',
- 'label': 'Show:',
- 'options' : [
- ('Successful builds', 'outcome:' + str(Build.SUCCEEDED), queryset_with_search.filter(outcome=str(Build.SUCCEEDED)).count()), # this is the field search expression
- ('Failed builds', 'outcome:'+ str(Build.FAILED), queryset_with_search.filter(outcome=str(Build.FAILED)).count()),
- ]
- }
- },
- {'name': 'Recipe', # default column, disabled box, with just the name in the list
- 'qhelp': "What you built (i.e. one or more recipes or image recipes)",
- 'orderfield': _get_toggle_order(request, "target__target"),
- 'ordericon':_get_toggle_order_icon(request, "target__target"),
- },
- {'name': 'Machine',
- 'qhelp': "The machine is the hardware for which you are building a recipe or image recipe",
- 'orderfield': _get_toggle_order(request, "machine"),
- 'ordericon':_get_toggle_order_icon(request, "machine"),
- 'dclass': 'span3'
- }, # a slightly wider column
- {'name': 'Started on', 'clclass': 'started_on', 'hidden' : 1, # this is an unchecked box, which hides the column
- 'qhelp': "The date and time you started the build",
- 'orderfield': _get_toggle_order(request, "started_on", True),
- 'ordericon':_get_toggle_order_icon(request, "started_on"),
- 'orderkey' : "started_on",
- 'filter' : {'class' : 'started_on',
- 'label': 'Show:',
- 'options' : [
- ("Today's builds" , 'started_on__gte:'+today_begin.strftime("%Y-%m-%d"), queryset_all.filter(started_on__gte=today_begin).count()),
- ("Yesterday's builds",
- 'started_on__gte!started_on__lt:'
- +yesterday_begin.strftime("%Y-%m-%d")+'!'
- +today_begin.strftime("%Y-%m-%d"),
- queryset_all.filter(
- started_on__gte=yesterday_begin,
- started_on__lt=today_begin
- ).count()),
- ("Build date range", 'daterange', 1, '', 'started_on'),
- ]
- }
- },
- {'name': 'Completed on',
- 'qhelp': "The date and time the build finished",
- 'orderfield': _get_toggle_order(request, "completed_on", True),
- 'ordericon':_get_toggle_order_icon(request, "completed_on"),
- 'orderkey' : 'completed_on',
- 'filter' : {'class' : 'completed_on',
- 'label': 'Show:',
- 'options' : [
- ("Today's builds" , 'completed_on__gte:'+today_begin.strftime("%Y-%m-%d"), queryset_all.filter(completed_on__gte=today_begin).count()),
- ("Yesterday's builds",
- 'completed_on__gte!completed_on__lt:'
- +yesterday_begin.strftime("%Y-%m-%d")+'!'
- +today_begin.strftime("%Y-%m-%d"),
- queryset_all.filter(
- completed_on__gte=yesterday_begin,
- completed_on__lt=today_begin
- ).count()),
- ("Build date range", 'daterange', 1, '', 'completed_on'),
- ]
- }
- },
- {'name': 'Failed tasks', 'clclass': 'failed_tasks', # specifing a clclass will enable the checkbox
- 'qhelp': "How many tasks failed during the build",
- 'filter' : {'class' : 'failed_tasks',
- 'label': 'Show:',
- 'options' : [
- ('Builds with failed tasks', 'task_build__outcome:4', queryset_with_search.filter(task_build__outcome=4).count()),
- ('Builds without failed tasks', 'task_build__outcome:NOT4', queryset_with_search.filter(~Q(task_build__outcome=4)).count()),
- ]
- }
- },
- {'name': 'Errors', 'clclass': 'errors_no',
- 'qhelp': "How many errors were encountered during the build (if any)",
- # Comment out sorting and filter until YOCTO #8131 is fixed
- #'orderfield': _get_toggle_order(request, "errors_no", True),
- #'ordericon':_get_toggle_order_icon(request, "errors_no"),
- #'orderkey' : 'errors_no',
- #'filter' : {'class' : 'errors_no',
- # 'label': 'Show:',
- # 'options' : [
- # ('Builds with errors', 'errors_no__gte:1', queryset_with_search.filter(errors_no__gte=1).count()),
- # ('Builds without errors', 'errors_no:0', queryset_with_search.filter(errors_no=0).count()),
- # ]
- # }
- },
- {'name': 'Warnings', 'clclass': 'warnings_no',
- 'qhelp': "How many warnings were encountered during the build (if any)",
- # Comment out sorting and filter until YOCTO #8131 is fixed
- #'orderfield': _get_toggle_order(request, "warnings_no", True),
- #'ordericon':_get_toggle_order_icon(request, "warnings_no"),
- #'orderkey' : 'warnings_no',
- #'filter' : {'class' : 'warnings_no',
- # 'label': 'Show:',
- # 'options' : [
- # ('Builds with warnings','warnings_no__gte:1', queryset_with_search.filter(warnings_no__gte=1).count()),
- # ('Builds without warnings','warnings_no:0', queryset_with_search.filter(warnings_no=0).count()),
- # ]
- # }
- },
- {'name': 'Time', 'clclass': 'time', 'hidden' : 1,
- 'qhelp': "How long it took the build to finish",
- # Comment out sorting until YOCTO #8131 is fixed
- #'orderfield': _get_toggle_order(request, "timespent", True),
- #'ordericon':_get_toggle_order_icon(request, "timespent"),
- #'orderkey' : 'timespent',
- },
- {'name': 'Image files', 'clclass': 'output',
- 'qhelp': "The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory",
- # TODO: compute image fstypes from Target_Image_File
- }
- ]
- }
-
- # merge daterange values
- context.update(context_date)
- return context, pagesize, orderby
-
-
-
# new project
def newproject(request):
template = "newproject.html"
@@ -2280,16 +2011,17 @@ if True:
prj.bitbake_version = prj.release.bitbake_version
prj.save()
# we need to change the layers
- for i in prj.projectlayer_set.all():
+ for project in prj.projectlayer_set.all():
# find and add a similarly-named layer on the new branch
try:
- lv = prj.compatible_layerversions(layer_name = i.layercommit.layer.name)[0]
- ProjectLayer.objects.get_or_create(project = prj, layercommit = lv)
+ layer_versions = prj.get_all_compatible_layer_versions()
+ layer_versions = layer_versions.filter(layer__name = project.layercommit.layer.name)
+ ProjectLayer.objects.get_or_create(project = prj, layercommit = layer_versions.first())
except IndexError:
pass
finally:
# get rid of the old entry
- i.delete()
+ project.delete()
if 'machineName' in request.POST:
machinevar = prj.projectvariable_set.get(name="MACHINE")
@@ -2316,7 +2048,7 @@ if True:
"completedbuilds": Build.objects.exclude(outcome = Build.IN_PROGRESS).filter(project_id = pid),
"prj" : {"name": prj.name, },
"buildrequests" : prj.build_set.filter(outcome=Build.IN_PROGRESS),
- "builds" : _project_recent_build_list(prj),
+ "builds" : Build.get_recent(prj),
"layers" : map(lambda x: {
"id": x.layercommit.pk,
"orderid": x.pk,
@@ -2371,13 +2103,23 @@ if True:
name = "_js_unit_test_prj_"
- # If there is an existing project by this name delete it. We don't want
- # Lots of duplicates cluttering up the projects.
+ # If there is an existing project by this name delete it.
+ # We don't want Lots of duplicates cluttering up the projects.
Project.objects.filter(name=name).delete()
- new_project = Project.objects.create_project(name=name, release=release)
+ new_project = Project.objects.create_project(name=name,
+ release=release)
+ # Add a layer
+ layer = new_project.get_all_compatible_layer_versions().first()
+
+ ProjectLayer.objects.get_or_create(layercommit=layer,
+ project=new_project)
- context = { 'project' : new_project }
+ # make sure we have a machine set for this project
+ ProjectVariable.objects.get_or_create(project=new_project,
+ name="MACHINE",
+ value="qemux86")
+ context = {'project': new_project}
return render(request, "js-unit-tests.html", context)
from django.views.decorators.csrf import csrf_exempt
@@ -2399,12 +2141,17 @@ if True:
retval = []
- for i in prj.projectlayer_set.all():
- lv = prj.compatible_layerversions(release = Release.objects.get(pk=new_release_id)).filter(layer__name = i.layercommit.layer.name)
+ for project in prj.projectlayer_set.all():
+ release = Release.objects.get(pk = new_release_id)
+
+ layer_versions = prj.get_all_compatible_layer_versions()
+ layer_versions = layer_versions.filter(release = release)
+ layer_versions = layer_versions.filter(layer__name = project.layercommit.layer.name)
+
# there is no layer_version with the new release id,
# and the same name
- if lv.count() < 1:
- retval.append(i)
+ if layer_versions.count() < 1:
+ retval.append(project)
return response({"error":"ok",
"rows" : map( _lv_to_dict(prj),
@@ -2461,6 +2208,10 @@ if True:
except ProjectVariable.DoesNotExist:
pass
try:
+ return_data['dl_dir'] = ProjectVariable.objects.get(project = prj, name = "DL_DIR").value,
+ except ProjectVariable.DoesNotExist:
+ pass
+ try:
return_data['fstypes'] = ProjectVariable.objects.get(project = prj, name = "IMAGE_FSTYPES").value,
except ProjectVariable.DoesNotExist:
pass
@@ -2473,7 +2224,7 @@ if True:
except ProjectVariable.DoesNotExist:
pass
try:
- return_data['sdk_machine'] = ProjectVariable.objects.get(project = prj, name = "SDKMACHINE").value,
+ return_data['sstate_dir'] = ProjectVariable.objects.get(project = prj, name = "SSTATE_DIR").value,
except ProjectVariable.DoesNotExist:
pass
@@ -2493,8 +2244,11 @@ if True:
layers_added = [];
# Rudimentary check for any possible html tags
- if "<" in request.POST:
- return HttpResponse(jsonfilter({"error": "Invalid character <"}), content_type = "application/json")
+ for val in request.POST.values():
+ if "<" in val:
+ return HttpResponse(jsonfilter(
+ {"error": "Invalid character <"}),
+ content_type="application/json")
prj = Project.objects.get(pk=request.POST['project_id'])
@@ -2669,33 +2423,106 @@ if True:
# create custom recipe
try:
- recipe = CustomImageRecipe.objects.create(
- name=request.POST["name"],
- base_recipe=params["base"],
- project=params["project"])
+
+ # Only allowed chars in name are a-z, 0-9 and -
+ if re.search(r'[^a-z|0-9|-]', request.POST["name"]):
+ return {"error": "invalid-name"}
+
+ custom_images = CustomImageRecipe.objects.all()
+
+ # Are there any recipes with this name already in our project?
+ existing_image_recipes_in_project = custom_images.filter(
+ name=request.POST["name"], project=params["project"])
+
+ if existing_image_recipes_in_project.count() > 0:
+ return {"error": "image-already-exists"}
+
+ # Are there any recipes with this name which aren't custom
+ # image recipes?
+ custom_image_ids = custom_images.values_list('id', flat=True)
+ existing_non_image_recipes = Recipe.objects.filter(
+ Q(name=request.POST["name"]) & ~Q(pk__in=custom_image_ids)
+ )
+
+ if existing_non_image_recipes.count() > 0:
+ return {"error": "recipe-already-exists"}
+
+ # create layer 'Custom layer' and verion if needed
+ layer = Layer.objects.get_or_create(
+ name=CustomImageRecipe.LAYER_NAME,
+ summary="Layer for custom recipes",
+ vcs_url="file:///toaster_created_layer")[0]
+
+ # Check if we have a layer version already
+ # We don't use get_or_create here because the dirpath will change
+ # and is a required field
+ lver = Layer_Version.objects.filter(Q(project=params['project']) &
+ Q(layer=layer) &
+ Q(build=None)).last()
+ if lver == None:
+ lver, created = Layer_Version.objects.get_or_create(
+ project=params['project'],
+ layer=layer,
+ dirpath="toaster_created_layer")
+
+ # Add a dependency on our layer to the base recipe's layer
+ LayerVersionDependency.objects.get_or_create(
+ layer_version=lver,
+ depends_on=params["base"].layer_version)
+
+ # Add it to our current project if needed
+ ProjectLayer.objects.get_or_create(project=params['project'],
+ layercommit=lver,
+ optional=False)
+
+ # Create the actual recipe
+ recipe, created = CustomImageRecipe.objects.get_or_create(
+ name=request.POST["name"],
+ base_recipe=params["base"],
+ project=params["project"],
+ layer_version=lver,
+ is_image=True)
+
+ # If we created the object then setup these fields. They may get
+ # overwritten later on and cause the get_or_create to create a
+ # duplicate if they've changed.
+ if created:
+ recipe.file_path = request.POST["name"]
+ recipe.license = "MIT"
+ recipe.version = "0.1"
+ recipe.save()
+
except Error as err:
return {"error": "Can't create custom recipe: %s" % err}
# Find the package list from the last build of this recipe/target
- build = Build.objects.filter(target__target=params['base'].name,
- project=params['project']).last()
-
- if build:
+ target = Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
+ Q(build__project=params['project']) &
+ (Q(target=params['base'].name) |
+ Q(target=recipe.name))).last()
+ if target:
# Copy in every package
# We don't want these packages to be linked to anything because
# that underlying data may change e.g. delete a build
- for package in build.package_set.all():
- # Create the duplicate
- package.pk = None
- package.save()
- # Disassociate the package from the build
- package.build = None
- package.save()
- recipe.packages.add(package)
- else:
- logger.warn("No packages found for this base recipe")
+ for tpackage in target.target_installed_package_set.all():
+ try:
+ built_package = tpackage.package
+ # The package had no recipe information so is a ghost
+ # package skip it
+ if built_package.recipe == None:
+ continue;
+
+ config_package = CustomImagePackage.objects.get(
+ name=built_package.name)
+
+ recipe.includes_set.add(config_package)
+ except Exception as e:
+ logger.warning("Error adding package %s %s" %
+ (tpackage.package.name, e))
+ pass
return {"error": "ok",
+ "packages" : recipe.get_all_packages().count(),
"url": reverse('customrecipe', args=(params['project'].pk,
recipe.id))}
@@ -2720,34 +2547,105 @@ if True:
or
{"error": <error message>}
"""
- objects = CustomImageRecipe.objects.filter(id=recipe_id)
- if not objects:
+ try:
+ custom_recipe = CustomImageRecipe.objects.get(id=recipe_id)
+ except CustomImageRecipe.DoesNotExist:
return {"error": "Custom recipe with id=%s "
"not found" % recipe_id}
+
if request.method == 'GET':
- values = CustomImageRecipe.objects.filter(id=recipe_id).values()
- if values:
- return {"error": "ok", "info": values[0]}
- else:
- return {"error": "Custom recipe with id=%s "
- "not found" % recipe_id}
- return {"error": "ok", "info": objects.values()[0]}
+ info = {"id" : custom_recipe.id,
+ "name" : custom_recipe.name,
+ "base_recipe_id": custom_recipe.base_recipe.id,
+ "project_id": custom_recipe.project.id,
+ }
+
+ return {"error": "ok", "info": info}
+
elif request.method == 'DELETE':
- objects.delete()
+ custom_recipe.delete()
return {"error": "ok"}
else:
return {"error": "Method %s is not supported" % request.method}
+ def customrecipe_download(request, pid, recipe_id):
+ recipe = get_object_or_404(CustomImageRecipe, pk=recipe_id)
+
+ file_data = recipe.generate_recipe_file_contents()
+
+ response = HttpResponse(file_data, content_type='text/plain')
+ response['Content-Disposition'] = \
+ 'attachment; filename="%s_%s.bb"' % (recipe.name,
+ recipe.version)
+
+ return response
+
+ def _traverse_dependents(next_package_id, rev_deps, all_current_packages, tree_level=0):
+ """
+ Recurse through reverse dependency tree for next_package_id.
+ Limit the reverse dependency search to packages not already scanned,
+ that is, not already in rev_deps.
+ Limit the scan to a depth (tree_level) not exceeding the count of
+ all packages in the custom image, and if that depth is exceeded
+ return False, pop out of the recursion, and write a warning
+ to the log, but this is unlikely, suggesting a dependency loop
+ not caught by bitbake.
+ On return, the input/output arg rev_deps is appended with queryset
+ dictionary elements, annotated for use in the customimage template.
+ The list has unsorted, but unique elements.
+ """
+ max_dependency_tree_depth = all_current_packages.count()
+ if tree_level >= max_dependency_tree_depth:
+ logger.warning(
+ "The number of reverse dependencies "
+ "for this package exceeds " + max_dependency_tree_depth +
+ " and the remaining reverse dependencies will not be removed")
+ return True
+
+ package = CustomImagePackage.objects.get(id=next_package_id)
+ dependents = \
+ package.package_dependencies_target.annotate(
+ name=F('package__name'),
+ pk=F('package__pk'),
+ size=F('package__size'),
+ ).values("name", "pk", "size").exclude(
+ ~Q(pk__in=all_current_packages)
+ )
+
+ for pkg in dependents:
+ if pkg in rev_deps:
+ # already seen, skip dependent search
+ continue
+
+ rev_deps.append(pkg)
+ if (_traverse_dependents(
+ pkg["pk"], rev_deps, all_current_packages, tree_level+1)):
+ return True
+
+ return False
+
+ def _get_all_dependents(package_id, all_current_packages):
+ """
+ Returns sorted list of recursive reverse dependencies for package_id,
+ as a list of dictionary items, by recursing through dependency
+ relationships.
+ """
+ rev_deps = []
+ _traverse_dependents(package_id, rev_deps, all_current_packages)
+ rev_deps = sorted(rev_deps, key=lambda x: x["name"])
+ return rev_deps
+
@xhr_response
def xhr_customrecipe_packages(request, recipe_id, package_id):
"""
ReST API to add/remove packages to/from custom recipe.
- Entry point: /xhr_customrecipe/<recipe_id>/packages/
+ Entry point: /xhr_customrecipe/<recipe_id>/packages/<package_id>
Methods:
PUT - Add package to the recipe
DELETE - Delete package from the recipe
+ GET - Get package information
Returns:
{"error": "ok"}
@@ -2760,26 +2658,149 @@ if True:
return {"error": "Custom recipe with id=%s "
"not found" % recipe_id}
- if request.method == 'GET' and not package_id:
- return {"error": "ok",
- "packages": list(recipe.packages.values_list('id'))}
+ if package_id:
+ try:
+ package = CustomImagePackage.objects.get(id=package_id)
+ except Package.DoesNotExist:
+ return {"error": "Package with id=%s "
+ "not found" % package_id}
- try:
- package = Package.objects.get(id=package_id)
- except Package.DoesNotExist:
- return {"error": "Package with id=%s "
- "not found" % package_id}
+ if request.method == 'GET':
+ # If no package_id then list the current packages
+ if not package_id:
+ total_size = 0
+ packages = recipe.get_all_packages().values("id",
+ "name",
+ "version",
+ "size")
+ for package in packages:
+ package['size_formatted'] = \
+ filtered_filesizeformat(package['size'])
+ total_size += package['size']
+
+ return {"error": "ok",
+ "packages" : list(packages),
+ "total" : len(packages),
+ "total_size" : total_size,
+ "total_size_formatted" :
+ filtered_filesizeformat(total_size)
+ }
+ else:
+ all_current_packages = recipe.get_all_packages()
+
+ # Dependencies for package which aren't satisfied by the
+ # current packages in the custom image recipe
+ deps = package.package_dependencies_source.annotate(
+ name=F('depends_on__name'),
+ pk=F('depends_on__pk'),
+ size=F('depends_on__size'),
+ ).values("name", "pk", "size").filter(
+ # There are two depends types we don't know why
+ (Q(dep_type=Package_Dependency.TYPE_TRDEPENDS) |
+ Q(dep_type=Package_Dependency.TYPE_RDEPENDS)) &
+ ~Q(pk__in=all_current_packages)
+ )
+
+ # Reverse dependencies which are needed by packages that are
+ # in the image. Recursive search providing all dependents,
+ # not just immediate dependents.
+ reverse_deps = _get_all_dependents(package_id, all_current_packages)
+ total_size_deps = 0
+ total_size_reverse_deps = 0
+
+ for dep in deps:
+ dep['size_formatted'] = \
+ filtered_filesizeformat(dep['size'])
+ total_size_deps += dep['size']
+
+ for dep in reverse_deps:
+ dep['size_formatted'] = \
+ filtered_filesizeformat(dep['size'])
+ total_size_reverse_deps += dep['size']
+
+
+ return {"error": "ok",
+ "id": package.pk,
+ "name": package.name,
+ "version": package.version,
+ "unsatisfied_dependencies": list(deps),
+ "unsatisfied_dependencies_size": total_size_deps,
+ "unsatisfied_dependencies_size_formatted":
+ filtered_filesizeformat(total_size_deps),
+ "reverse_dependencies": list(reverse_deps),
+ "reverse_dependencies_size": total_size_reverse_deps,
+ "reverse_dependencies_size_formatted":
+ filtered_filesizeformat(total_size_reverse_deps)}
+
+ included_packages = recipe.includes_set.values_list('pk', flat=True)
if request.method == 'PUT':
- recipe.packages.add(package)
+ # If we're adding back a package which used to be included in this
+ # image all we need to do is remove it from the excludes
+ if package.pk in included_packages:
+ try:
+ recipe.excludes_set.remove(package)
+ return {"error": "ok"}
+ except Package.DoesNotExist:
+ return {"error":
+ "Package %s not found in excludes but was in "
+ "included list" % package.name}
+
+ else:
+ recipe.appends_set.add(package)
+ # Make sure that package is not in the excludes set
+ try:
+ recipe.excludes_set.remove(package)
+ except:
+ pass
+ # Add the dependencies we think will be added to the recipe
+ # as a result of appending this package.
+ # TODO this should recurse down the entire deps tree
+ for dep in package.package_dependencies_source.all_depends():
+ try:
+ cust_package = CustomImagePackage.objects.get(
+ name=dep.depends_on.name)
+
+ recipe.includes_set.add(cust_package)
+ try:
+ # When adding the pre-requisite package, make
+ # sure it's not in the excluded list from a
+ # prior removal.
+ recipe.excludes_set.remove(cust_package)
+ except Package.DoesNotExist:
+ # Don't care if the package had never been excluded
+ pass
+ except:
+ logger.warning("Could not add package's suggested"
+ "dependencies to the list")
+
return {"error": "ok"}
+
elif request.method == 'DELETE':
- if package in recipe.packages.all():
- recipe.packages.remove(package)
+ try:
+ # If we're deleting a package which is included we need to
+ # Add it to the excludes list.
+ if package.pk in included_packages:
+ recipe.excludes_set.add(package)
+ else:
+ recipe.appends_set.remove(package)
+ all_current_packages = recipe.get_all_packages()
+ reverse_deps_dictlist = _get_all_dependents(package.pk, all_current_packages)
+ ids = [entry['pk'] for entry in reverse_deps_dictlist]
+ reverse_deps = CustomImagePackage.objects.filter(id__in=ids)
+ for r in reverse_deps:
+ try:
+ if r.id in included_packages:
+ recipe.excludes_set.add(r)
+ else:
+ recipe.appends_set.remove(r)
+ except:
+ pass
+
return {"error": "ok"}
- else:
- return {"error": "Package '%s' is not in the recipe '%s'" % \
- (package.name, recipe.name)}
+ except CustomImageRecipe.DoesNotExist:
+ return {"error": "Tried to remove package that wasn't present"}
+
else:
return {"error": "Method %s is not supported" % request.method}
@@ -2816,30 +2837,16 @@ if True:
}
vars_blacklist = {
- 'DL_DR','PARALLEL_MAKE','BB_NUMBER_THREADS','SSTATE_DIR',
+ 'PARALLEL_MAKE','BB_NUMBER_THREADS',
'BB_DISKMON_DIRS','BB_NUMBER_THREADS','CVS_PROXY_HOST','CVS_PROXY_PORT',
- 'DL_DIR','PARALLEL_MAKE','SSTATE_DIR','SSTATE_DIR','SSTATE_MIRRORS','TMPDIR',
+ 'PARALLEL_MAKE','SSTATE_MIRRORS','TMPDIR',
'all_proxy','ftp_proxy','http_proxy ','https_proxy'
}
- vars_fstypes = {
- 'btrfs','cpio','cpio.gz','cpio.lz4','cpio.lzma','cpio.xz','cramfs',
- 'elf','ext2','ext2.bz2','ext2.gz','ext2.lzma', 'ext4', 'ext4.gz', 'ext3','ext3.gz','hddimg',
- 'iso','jffs2','jffs2.sum','squashfs','squashfs-lzo','squashfs-xz','tar.bz2',
- 'tar.lz4','tar.xz','tartar.gz','ubi','ubifs','vmdk'
- }
+ vars_fstypes = Target_Image_File.SUFFIXES
return(vars_managed,sorted(vars_fstypes),vars_blacklist)
- def customrecipe(request, pid, recipe_id):
- project = Project.objects.get(pk=pid)
- context = {'project' : project,
- 'projectlayers': [],
- 'recipe' : CustomImageRecipe.objects.get(pk=recipe_id)
- }
-
- return render(request, "customrecipe.html", context)
-
@_template_renderer("projectconf.html")
def projectconf(request, pid):
@@ -2870,6 +2877,19 @@ if True:
except ProjectVariable.DoesNotExist:
pass
try:
+ if ProjectVariable.objects.get(project = prj, name = "DL_DIR").value == "${TOPDIR}/../downloads":
+ be = BuildEnvironment.objects.get(pk = str(1))
+ dl_dir = os.path.join(dirname(be.builddir), "downloads")
+ context['dl_dir'] = dl_dir
+ pv, created = ProjectVariable.objects.get_or_create(project = prj, name = "DL_DIR")
+ pv.value = dl_dir
+ pv.save()
+ else:
+ context['dl_dir'] = ProjectVariable.objects.get(project = prj, name = "DL_DIR").value
+ context['dl_dir_defined'] = "1"
+ except ProjectVariable.DoesNotExist,BuildEnvironment.DoesNotExist:
+ pass
+ try:
context['fstypes'] = ProjectVariable.objects.get(project = prj, name = "IMAGE_FSTYPES").value
context['fstypes_defined'] = "1"
except ProjectVariable.DoesNotExist:
@@ -2885,268 +2905,78 @@ if True:
except ProjectVariable.DoesNotExist:
pass
try:
- context['sdk_machine'] = ProjectVariable.objects.get(project = prj, name = "SDKMACHINE").value
- context['sdk_machine_defined'] = "1"
- except ProjectVariable.DoesNotExist:
+ if ProjectVariable.objects.get(project = prj, name = "SSTATE_DIR").value == "${TOPDIR}/../sstate-cache":
+ be = BuildEnvironment.objects.get(pk = str(1))
+ sstate_dir = os.path.join(dirname(be.builddir), "sstate-cache")
+ context['sstate_dir'] = sstate_dir
+ pv, created = ProjectVariable.objects.get_or_create(project = prj, name = "SSTATE_DIR")
+ pv.value = sstate_dir
+ pv.save()
+ else:
+ context['sstate_dir'] = ProjectVariable.objects.get(project = prj, name = "SSTATE_DIR").value
+ context['sstate_dir_defined'] = "1"
+ except ProjectVariable.DoesNotExist, BuildEnvironment.DoesNotExist:
pass
return context
- # WARNING _build_list_helper() may raise a RedirectException, which
- # will set the GET parameters and redirect back to the
- # all-builds or projectbuilds page as appropriate;
- # TODO don't use exceptions to control program flow
- @_template_renderer('projectbuilds.html')
- def projectbuilds(request, pid):
- prj = Project.objects.get(id = pid)
-
- if request.method == "POST":
- # process any build request
-
- if 'buildCancel' in request.POST:
- for i in request.POST['buildCancel'].strip().split(" "):
- try:
- br = BuildRequest.objects.select_for_update().get(project = prj, pk = i, state__lte = BuildRequest.REQ_QUEUED)
- br.state = BuildRequest.REQ_DELETED
- br.save()
- except BuildRequest.DoesNotExist:
- pass
-
- if 'buildDelete' in request.POST:
- for i in request.POST['buildDelete'].strip().split(" "):
- try:
- BuildRequest.objects.select_for_update().get(project = prj, pk = i, state__lte = BuildRequest.REQ_DELETED).delete()
- except BuildRequest.DoesNotExist:
- pass
-
- if 'targets' in request.POST:
- ProjectTarget.objects.filter(project = prj).delete()
- s = str(request.POST['targets'])
- for t in s.translate(None, ";%|\"").split(" "):
- if ":" in t:
- target, task = t.split(":")
- else:
- target = t
- task = ""
- ProjectTarget.objects.create(project = prj,
- target = target,
- task = task)
- prj.schedule_build()
-
- queryset = Build.objects.filter(project_id = pid)
-
- redirect_page = resolve(request.path_info).url_name
-
- context, pagesize, orderby = _build_list_helper(request,
- queryset,
- redirect_page,
- pid)
-
- context['project'] = prj
- _set_parameters_values(pagesize, orderby, request)
-
- # add the most recent builds for this project
- context['mru'] = _get_latest_builds(prj)
-
- return context
+ def _file_names_for_artifact(build, artifact_type, artifact_id):
+ """
+ Return a tuple (file path, file name for the download response) for an
+ artifact of type artifact_type with ID artifact_id for build; if
+ artifact type is not supported, returns (None, None)
+ """
+ file_name = None
+ response_file_name = None
+ if artifact_type == "cookerlog":
+ file_name = build.cooker_log_path
+ response_file_name = "cooker.log"
- def _file_name_for_artifact(b, artifact_type, artifact_id):
- file_name = None
- # Target_Image_File file_name
- if artifact_type == "imagefile":
- file_name = Target_Image_File.objects.get(target__build = b, pk = artifact_id).file_name
+ elif artifact_type == "imagefile":
+ file_name = Target_Image_File.objects.get(target__build = build, pk = artifact_id).file_name
elif artifact_type == "buildartifact":
- file_name = BuildArtifact.objects.get(build = b, pk = artifact_id).file_name
+ file_name = BuildArtifact.objects.get(build = build, pk = artifact_id).file_name
- elif artifact_type == "licensemanifest":
- file_name = Target.objects.get(build = b, pk = artifact_id).license_manifest_path
+ elif artifact_type == "licensemanifest":
+ file_name = Target.objects.get(build = build, pk = artifact_id).license_manifest_path
elif artifact_type == "tasklogfile":
- file_name = Task.objects.get(build = b, pk = artifact_id).logfile
+ file_name = Task.objects.get(build = build, pk = artifact_id).logfile
elif artifact_type == "logmessagefile":
- file_name = LogMessage.objects.get(build = b, pk = artifact_id).pathname
- else:
- raise Exception("FIXME: artifact type %s not implemented" % (artifact_type))
+ file_name = LogMessage.objects.get(build = build, pk = artifact_id).pathname
- return file_name
+ if file_name and not response_file_name:
+ response_file_name = os.path.basename(file_name)
+ return (file_name, response_file_name)
def build_artifact(request, build_id, artifact_type, artifact_id):
- if artifact_type in ["cookerlog"]:
- try:
- build = Build.objects.get(pk = build_id)
- file_name = build.cooker_log_path
+ """
+ View which returns a build artifact file as a response
+ """
+ file_name = None
+ response_file_name = None
+
+ try:
+ build = Build.objects.get(pk = build_id)
+ file_name, response_file_name = _file_names_for_artifact(
+ build, artifact_type, artifact_id
+ )
+
+ if file_name and response_file_name:
fsock = open(file_name, "r")
content_type = MimeTypeFinder.get_mimetype(file_name)
response = HttpResponse(fsock, content_type = content_type)
- disposition = 'attachment; filename=cooker.log'
- response['Content-Disposition'] = disposition
+ disposition = "attachment; filename=" + response_file_name
+ response["Content-Disposition"] = disposition
return response
- except IOError:
- context = {
- 'build' : Build.objects.get(pk = build_id),
- }
- return render(request, "unavailable_artifact.html", context)
-
- else:
- # retrieve the artifact directly from the build environment
- return _get_be_artifact(request, build_id, artifact_type, artifact_id)
-
-
- def _get_be_artifact(request, build_id, artifact_type, artifact_id):
- try:
- b = Build.objects.get(pk=build_id)
- if b.buildrequest is None or b.buildrequest.environment is None:
- raise Exception("Artifact not available for download (missing build request or build environment)")
-
- file_name = _file_name_for_artifact(b, artifact_type, artifact_id)
- fsock = None
- content_type='application/force-download'
-
- if file_name is None:
- raise Exception("Could not handle artifact %s id %s" % (artifact_type, artifact_id))
else:
- content_type = MimeTypeFinder.get_mimetype(file_name)
- fsock = b.buildrequest.environment.get_artifact(file_name)
- file_name = os.path.basename(file_name) # we assume that the build environment system has the same path conventions as host
-
- response = HttpResponse(fsock, content_type = content_type)
-
- # returns a file from the environment
- response['Content-Disposition'] = 'attachment; filename=' + file_name
- return response
- except IOError:
- context = {
- 'build' : Build.objects.get(pk = build_id),
- }
- return render(request, "unavailable_artifact.html", context)
-
-
-
-
- @_template_renderer("projects.html")
- def projects(request):
- (pagesize, orderby) = _get_parameters_values(request, 10, 'updated:-')
- mandatory_parameters = { 'count': pagesize, 'page' : 1, 'orderby' : orderby }
- retval = _verify_parameters( request.GET, mandatory_parameters )
- if retval:
- raise RedirectException( 'all-projects', request.GET, mandatory_parameters )
-
- queryset_all = Project.objects.all()
-
- # annotate each project with its number of builds
- queryset_all = queryset_all.annotate(num_builds=Count('build'))
-
- # exclude the command line builds project if it has no builds
- q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0)
- queryset_all = queryset_all.filter(Q(is_default=False) |
- q_default_with_builds)
-
- # if in BUILD_MODE, exclude everything but the command line builds project
- if not toastermain.settings.BUILD_MODE:
- queryset_all = queryset_all.exclude(is_default=False)
-
- # boilerplate code that takes a request for an object type and returns a queryset
- # for that object type. copypasta for all needed table searches
- (filter_string, search_term, ordering_string) = _search_tuple(request, Project)
- queryset_with_search = _get_queryset(Project, queryset_all, None, search_term, ordering_string, '-updated')
- queryset = _get_queryset(Project, queryset_all, filter_string, search_term, ordering_string, '-updated')
-
- # retrieve the objects that will be displayed in the table; projects a paginator and gets a page range to display
- project_info = _build_page_range(Paginator(queryset, pagesize), request.GET.get('page', 1))
-
- # add fields needed in JSON dumps for API call support
- for p in project_info.object_list:
- p.id = p.pk
- p.projectPageUrl = reverse('project', args=(p.id,))
- p.layersTypeAheadUrl = reverse('xhr_layerstypeahead', args=(p.id,))
- p.recipesTypeAheadUrl = reverse('xhr_recipestypeahead', args=(p.id,))
- p.projectBuildsUrl = reverse('projectbuilds', args=(p.id,))
-
- # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds)
- build_mru = _get_latest_builds()
-
- # translate the project's build target strings
- fstypes_map = {};
- for project in project_info:
- try:
- targets = Target.objects.filter( build_id = project.get_last_build_id() )
- comma = "";
- extensions = "";
- for t in targets:
- if ( not t.is_image ):
- continue
- tif = Target_Image_File.objects.filter( target_id = t.id )
- for i in tif:
- s=re.sub('.*tar.bz2', 'tar.bz2', i.file_name)
- if s == i.file_name:
- s=re.sub('.*\.', '', i.file_name)
- if None == re.search(s,extensions):
- extensions += comma + s
- comma = ", "
- fstypes_map[project.id]=extensions
- except (Target.DoesNotExist,IndexError):
- fstypes_map[project.id]=project.get_last_imgfiles
-
- context = {
- 'mru' : build_mru,
-
- 'objects' : project_info,
- 'objectname' : "projects",
- 'default_orderby' : 'id:-',
- 'search_term' : search_term,
- 'total_count' : queryset_with_search.count(),
- 'fstypes' : fstypes_map,
- 'build_FAILED' : Build.FAILED,
- 'build_SUCCEEDED' : Build.SUCCEEDED,
- 'tablecols': [
- {'name': 'Project',
- 'orderfield': _get_toggle_order(request, "name"),
- 'ordericon':_get_toggle_order_icon(request, "name"),
- 'orderkey' : 'name',
- },
- {'name': 'Last activity on',
- 'clclass': 'updated',
- 'qhelp': "Shows the starting date and time of the last project build. If the project has no builds, it shows the date the project was created",
- 'orderfield': _get_toggle_order(request, "updated", True),
- 'ordericon':_get_toggle_order_icon(request, "updated"),
- 'orderkey' : 'updated',
- },
- {'name': 'Release',
- 'qhelp' : "The version of the build system used by the project",
- 'orderfield': _get_toggle_order(request, "release__name"),
- 'ordericon':_get_toggle_order_icon(request, "release__name"),
- 'orderkey' : 'release__name',
- },
- {'name': 'Machine',
- 'qhelp': "The hardware currently selected for the project",
- },
- {'name': 'Number of builds',
- 'qhelp': "How many builds have been run for the project",
- },
- {'name': 'Last build outcome', 'clclass': 'loutcome',
- 'qhelp': "Tells you if the last project build completed successfully or failed",
- },
- {'name': 'Recipe', 'clclass': 'ltarget',
- 'qhelp': "The last recipe that was built in this project",
- },
- {'name': 'Errors', 'clclass': 'lerrors',
- 'qhelp': "How many errors were encountered during the last project build (if any)",
- },
- {'name': 'Warnings', 'clclass': 'lwarnings',
- 'qhelp': "How many warnigns were encountered during the last project build (if any)",
- },
- {'name': 'Image files', 'clclass': 'limagefiles', 'hidden': 1,
- 'qhelp': "The root file system types produced by the last project build",
- },
- ]
- }
-
- _set_parameters_values(pagesize, orderby, request)
- return context
+ return render(request, "unavailable_artifact.html")
+ except ObjectDoesNotExist, IOError:
+ return render(request, "unavailable_artifact.html")
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/widgets.py b/yocto-poky/bitbake/lib/toaster/toastergui/widgets.py
index 6bb388936..d2ef5d3db 100644
--- a/yocto-poky/bitbake/lib/toaster/toastergui/widgets.py
+++ b/yocto-poky/bitbake/lib/toaster/toastergui/widgets.py
@@ -38,11 +38,13 @@ import json
import collections
import operator
import re
+import urllib
import logging
logger = logging.getLogger("toaster")
from toastergui.views import objtojson
+from toastergui.tablefilter import TableFilterMap
class ToasterTable(TemplateView):
def __init__(self, *args, **kwargs):
@@ -52,10 +54,12 @@ class ToasterTable(TemplateView):
self.title = "Table"
self.queryset = None
self.columns = []
- self.filters = {}
+
+ # map from field names to Filter instances
+ self.filter_map = TableFilterMap()
+
self.total_count = 0
self.static_context_extra = {}
- self.filter_actions = {}
self.empty_state = "Sorry - no data found"
self.default_orderby = ""
@@ -65,7 +69,7 @@ class ToasterTable(TemplateView):
orderable=True,
field_name="id")
- # prevent HTTP caching of table data
+ # prevent HTTP caching of table data
@cache_control(must_revalidate=True, max_age=0, no_store=True, no_cache=True)
def dispatch(self, *args, **kwargs):
return super(ToasterTable, self).dispatch(*args, **kwargs)
@@ -107,26 +111,10 @@ class ToasterTable(TemplateView):
self.apply_search(search)
name = request.GET.get("name", None)
- if name is None:
- data = json.dumps(self.filters,
- indent=2,
- cls=DjangoJSONEncoder)
- else:
- for actions in self.filters[name]['filter_actions']:
- actions['count'] = self.filter_actions[actions['name']](count_only=True)
-
- # Add the "All" items filter action
- self.filters[name]['filter_actions'].insert(0, {
- 'name' : 'all',
- 'title' : 'All',
- 'count' : self.queryset.count(),
- })
-
- data = json.dumps(self.filters[name],
- indent=2,
- cls=DjangoJSONEncoder)
-
- return data
+ table_filter = self.filter_map.get_filter(name)
+ return json.dumps(table_filter.to_json(self.queryset),
+ indent=2,
+ cls=DjangoJSONEncoder)
def setup_columns(self, *args, **kwargs):
""" function to implement in the subclass which sets up the columns """
@@ -138,30 +126,13 @@ class ToasterTable(TemplateView):
""" function to implement in the subclass which sets up the queryset"""
pass
- def add_filter(self, name, title, filter_actions):
+ def add_filter(self, table_filter):
"""Add a filter to the table.
Args:
- name (str): Unique identifier of the filter.
- title (str): Title of the filter.
- filter_actions: Actions for all the filters.
+ table_filter: Filter instance
"""
- self.filters[name] = {
- 'title' : title,
- 'filter_actions' : filter_actions,
- }
-
- def make_filter_action(self, name, title, action_function):
- """ Utility to make a filter_action """
-
- action = {
- 'title' : title,
- 'name' : name,
- }
-
- self.filter_actions[name] = action_function
-
- return action
+ self.filter_map.add_filter(table_filter.name, table_filter)
def add_column(self, title="", help_text="",
orderable=False, hideable=True, hidden=False,
@@ -197,6 +168,24 @@ class ToasterTable(TemplateView):
'computation': computation,
})
+ def set_column_hidden(self, title, hidden):
+ """
+ Set the hidden state of the column to the value of hidden
+ """
+ for col in self.columns:
+ if col['title'] == title:
+ col['hidden'] = hidden
+ break
+
+ def set_column_hideable(self, title, hideable):
+ """
+ Set the hideable state of the column to the value of hideable
+ """
+ for col in self.columns:
+ if col['title'] == title:
+ col['hideable'] = hideable
+ break
+
def render_static_data(self, template, row):
"""Utility function to render the static data template"""
@@ -210,19 +199,35 @@ class ToasterTable(TemplateView):
return template.render(context)
- def apply_filter(self, filters, **kwargs):
+ def apply_filter(self, filters, filter_value, **kwargs):
+ """
+ Apply a filter submitted in the querystring to the ToasterTable
+
+ filters: (str) in the format:
+ '<filter name>:<action name>'
+ filter_value: (str) parameters to pass to the named filter
+
+ <filter name> and <action name> are used to look up the correct filter
+ in the ToasterTable's filter map; the <action params> are set on
+ TableFilterAction* before its filter is applied and may modify the
+ queryset returned by the filter
+ """
self.setup_filters(**kwargs)
try:
- filter_name, filter_action = filters.split(':')
+ filter_name, action_name = filters.split(':')
+ action_params = urllib.unquote_plus(filter_value)
except ValueError:
return
- if "all" in filter_action:
+ if "all" in action_name:
return
try:
- self.filter_actions[filter_action]()
+ table_filter = self.filter_map.get_filter(filter_name)
+ action = table_filter.get_action(action_name)
+ action.set_filter_params(action_params)
+ self.queryset = action.filter(self.queryset)
except KeyError:
# pass it to the user - programming error here
raise
@@ -251,13 +256,20 @@ class ToasterTable(TemplateView):
def get_data(self, request, **kwargs):
- """Returns the data for the page requested with the specified
- parameters applied"""
+ """
+ Returns the data for the page requested with the specified
+ parameters applied
+
+ filters: filter and action name, e.g. "outcome:build_succeeded"
+ filter_value: value to pass to the named filter+action, e.g. "on"
+ (for a toggle filter) or "2015-12-11,2015-12-12" (for a date range filter)
+ """
page_num = request.GET.get("page", 1)
limit = request.GET.get("limit", 10)
search = request.GET.get("search", None)
filters = request.GET.get("filter", None)
+ filter_value = request.GET.get("filter_value", "on")
orderby = request.GET.get("orderby", None)
nocache = request.GET.get("nocache", None)
@@ -289,7 +301,7 @@ class ToasterTable(TemplateView):
if search:
self.apply_search(search)
if filters:
- self.apply_filter(filters, **kwargs)
+ self.apply_filter(filters, filter_value, **kwargs)
if orderby:
self.apply_orderby(orderby)
OpenPOWER on IntegriCloud