From 2251426ae420640c082ec0d0109b9be435075411 Mon Sep 17 00:00:00 2001 From: Alexandru DAMIAN Date: Wed, 11 Dec 2013 16:42:34 +0000 Subject: bitbake: toaster: Create the base page navigation structure Updating the general container pages to use the graphical design and features from the design phase. In the process of adapting the Simple UI to the designed interface, we create all the pages and the navigation structure for the Toaster GUI. Views for each page have been added, and the url mapping has been updated to reflect newly added pages. The table page has been refactored to be component-oriented instead of class-oriented in order to facilitate reusage. Changes are made in different layers of the template (base, basetable) in order to maximize code reuse among different pages in the build. (Bitbake rev: d31f039ae31b77023722c06e66542751536a1362) Signed-off-by: Alexandru DAMIAN Signed-off-by: Richard Purdie --- bitbake/lib/toaster/toastergui/templates/base.html | 64 ++++++-- .../toastergui/templates/basebuildpage.html | 69 +++++++-- .../toaster/toastergui/templates/basetable.html | 64 -------- .../toastergui/templates/basetable_bottom.html | 60 ++++++++ .../toastergui/templates/basetable_top.html | 66 ++++++++ .../lib/toaster/toastergui/templates/bpackage.html | 12 +- .../lib/toaster/toastergui/templates/build.html | 115 ++++++++++---- .../toastergui/templates/builddashboard.html | 8 + .../toaster/toastergui/templates/buildtime.html | 4 + .../toastergui/templates/configuration.html | 10 +- .../lib/toaster/toastergui/templates/cpuusage.html | 4 + .../lib/toaster/toastergui/templates/diskio.html | 4 + .../lib/toaster/toastergui/templates/recipe.html | 15 +- .../lib/toaster/toastergui/templates/target.html | 8 + bitbake/lib/toaster/toastergui/templates/task.html | 11 +- .../toaster/toastergui/templatetags/projecttags.py | 5 + bitbake/lib/toaster/toastergui/urls.py | 34 ++++- bitbake/lib/toaster/toastergui/views.py | 167 +++++++++++++++++++-- bitbake/lib/toaster/toastermain/settings.py | 10 ++ 19 files changed, 571 insertions(+), 159 deletions(-) delete mode 100644 bitbake/lib/toaster/toastergui/templates/basetable.html create mode 100644 bitbake/lib/toaster/toastergui/templates/basetable_bottom.html create mode 100644 bitbake/lib/toaster/toastergui/templates/basetable_top.html create mode 100644 bitbake/lib/toaster/toastergui/templates/builddashboard.html create mode 100644 bitbake/lib/toaster/toastergui/templates/buildtime.html create mode 100644 bitbake/lib/toaster/toastergui/templates/cpuusage.html create mode 100644 bitbake/lib/toaster/toastergui/templates/diskio.html create mode 100644 bitbake/lib/toaster/toastergui/templates/target.html (limited to 'bitbake/lib') diff --git a/bitbake/lib/toaster/toastergui/templates/base.html b/bitbake/lib/toaster/toastergui/templates/base.html index d58cbeaed5..3508962e67 100644 --- a/bitbake/lib/toaster/toastergui/templates/base.html +++ b/bitbake/lib/toaster/toastergui/templates/base.html @@ -1,30 +1,64 @@ {% load static %} - - Toaster Simple Explorer - - - - + + + + -
- - -{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html b/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html new file mode 100644 index 0000000000..2a6f084929 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html @@ -0,0 +1,60 @@ + + + + + + + + diff --git a/bitbake/lib/toaster/toastergui/templates/basetable_top.html b/bitbake/lib/toaster/toastergui/templates/basetable_top.html new file mode 100644 index 0000000000..b9277b4a3d --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/basetable_top.html @@ -0,0 +1,66 @@ + + + + + + + + + diff --git a/bitbake/lib/toaster/toastergui/templates/bpackage.html b/bitbake/lib/toaster/toastergui/templates/bpackage.html index 67fc65ca3e..3329ddae51 100644 --- a/bitbake/lib/toaster/toastergui/templates/bpackage.html +++ b/bitbake/lib/toaster/toastergui/templates/bpackage.html @@ -1,7 +1,12 @@ {% extends "basebuildpage.html" %} -{% block pagetitle %}Packages{% endblock %} -{% block pagetable %} +{% block localbreadcrumb %} +
  • Packages
  • +{% endblock %} + +{% block buildinfomain %} +{% include "basetable_top.html" %} + {% if not objects %}

    No packages were recorded for this target!

    {% else %} @@ -21,7 +26,7 @@ {% for package in objects %} - + @@ -41,4 +46,5 @@ {% endif %} +{% include "basetable_bottom.html" %} {% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/build.html b/bitbake/lib/toaster/toastergui/templates/build.html index 4fa87d5271..27ce1ccbc5 100644 --- a/bitbake/lib/toaster/toastergui/templates/build.html +++ b/bitbake/lib/toaster/toastergui/templates/build.html @@ -1,43 +1,96 @@ -{% extends "basetable.html" %} +{% extends "base.html" %} + + +{% load projecttags %} +{% load humanize %} + +{% block pagecontent %} +
    + + +{{build_mru}} +{% for build in mru %} +
    +
    + +{%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} +
    +{% if build.errors_no %} + {{build.errors_no}} error{{build.errors_no|pluralize}} +{% endif %} +
    +
    +{% if build.warnings_no %} + {{build.warnings_no}} warning{{build.warnings_no|pluralize}} +{% endif %} +
    +
    + Build time: {{ build|timespent }} +
    +{%endif%}{%if build.outcome == build.IN_PROGRESS %} +
    +
    +
    +
    +
    +
    ETA: in {{build.eta|naturaltime}}
    +{%endif%} +
    +
    + +{% endfor %} -{% block pagename %} -

    Toaster - Builds

    -{% endblock %} -{% block pagetable %} + + +{% include "basetable_top.html" %} - {% load projecttags %}
    - - - - - - - - - - - - + + + + + + + + + + + + {% for build in objects %} - - - - - - - - - - - - + + + + + + + + + + + {% endfor %} -{% endblock %} +{% include "basetable_bottom.html" %} + + +{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/builddashboard.html b/bitbake/lib/toaster/toastergui/templates/builddashboard.html new file mode 100644 index 0000000000..7c58cc0b25 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/builddashboard.html @@ -0,0 +1,8 @@ +{% extends "basebuildpage.html" %} +{% block localbreadcrumb %} +
  • Dashboard
  • +{% endblock %} + +{% block buildinfomain %} + +{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/buildtime.html b/bitbake/lib/toaster/toastergui/templates/buildtime.html new file mode 100644 index 0000000000..ea84ae797c --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/buildtime.html @@ -0,0 +1,4 @@ +{% extends "basebuildpage.html" %} +{% block localbreadcrumb %} +
  • Build Time
  • +{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/configuration.html b/bitbake/lib/toaster/toastergui/templates/configuration.html index 521620fdce..e390a95ff5 100644 --- a/bitbake/lib/toaster/toastergui/templates/configuration.html +++ b/bitbake/lib/toaster/toastergui/templates/configuration.html @@ -1,7 +1,11 @@ {% extends "basebuildpage.html" %} +{% block localbreadcrumb %} +
  • Configuration
  • +{% endblock %} + +{% block buildinfomain %} -{% block pagetitle %}Configuration{% endblock %} -{% block pagetable %} +{% include "basetable_top.html" %} @@ -19,4 +23,6 @@ {% endfor %} +{% include "basetable_bottom.html" %} + {% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/cpuusage.html b/bitbake/lib/toaster/toastergui/templates/cpuusage.html new file mode 100644 index 0000000000..02f07b7605 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/cpuusage.html @@ -0,0 +1,4 @@ +{% extends "basebuildpage.html" %} +{% block localbreadcrumb %} +
  • Cpu Usage
  • +{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/diskio.html b/bitbake/lib/toaster/toastergui/templates/diskio.html new file mode 100644 index 0000000000..c5cef6f385 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/diskio.html @@ -0,0 +1,4 @@ +{% extends "basebuildpage.html" %} +{% block localbreadcrumb %} +
  • Disk I/O
  • +{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/recipe.html b/bitbake/lib/toaster/toastergui/templates/recipe.html index d7f57eb9ea..87c69b8d3c 100644 --- a/bitbake/lib/toaster/toastergui/templates/recipe.html +++ b/bitbake/lib/toaster/toastergui/templates/recipe.html @@ -1,14 +1,11 @@ -{% extends "basetable.html" %} +{% extends "basebuildpage.html" %} -{% block pagename %} - -

    Toaster - Recipes for a Layer

    +{% block localbreadcrumb %} +
  • Recipes
  • {% endblock %} -{% block pagetable %} - {% load projecttags %} +{% block buildinfomain %} +{% include "basetable_top.html" %} @@ -49,4 +46,6 @@ {% endfor %} +{% include "basetable_bottom.html" %} + {% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/target.html b/bitbake/lib/toaster/toastergui/templates/target.html new file mode 100644 index 0000000000..f2d0ad461b --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/target.html @@ -0,0 +1,8 @@ +{% extends "basebuildpage.html" %} + +{% block localbreadcrumb %} +
  • Target
  • +{% endblock %} + +{% block buildinfomain %} +{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/task.html b/bitbake/lib/toaster/toastergui/templates/task.html index de965ab797..6af2c51277 100644 --- a/bitbake/lib/toaster/toastergui/templates/task.html +++ b/bitbake/lib/toaster/toastergui/templates/task.html @@ -1,7 +1,13 @@ {% extends "basebuildpage.html" %} -{% block pagetitle %}Tasks{% endblock %} -{% block pagetable %} +{% block localbreadcrumb %} +
  • Tasks
  • +{% endblock %} + +{% block buildinfomain %} +{% include "basetable_top.html" %} + + {% if not objects %}

    No tasks were executed in this build!

    {% else %} @@ -64,4 +70,5 @@ {% endif %} +{% include "basetable_bottom.html" %} {% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py index 0c0d804c0c..5f60379932 100644 --- a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py +++ b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py @@ -24,3 +24,8 @@ register = template.Library() @register.simple_tag def time_difference(start_time, end_time): return end_time - start_time + +@register.filter(name = 'timespent') +def timespent(build_object): + tdsec = (build_object.completed_on - build_object.started_on).total_seconds() + return "%02d:%02d:%02d" % (int(tdsec/3600), int((tdsec - tdsec/ 3600)/ 60), int(tdsec) % 60) diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py index b84c95f08b..f531eb0137 100644 --- a/bitbake/lib/toaster/toastergui/urls.py +++ b/bitbake/lib/toaster/toastergui/urls.py @@ -19,14 +19,34 @@ from django.conf.urls import patterns, include, url from django.views.generic import RedirectView -urlpatterns = patterns('bldviewer.views', - url(r'^builds/$', 'build', name='all-builds'), - url(r'^build/(?P\d+)/task/$', 'task', name='task'), - url(r'^build/(?P\d+)/packages/$', 'bpackage', name='bpackage'), - url(r'^build/(?P\d+)/package/(?P\d+)/files/$', 'bfile', name='bfile'), - url(r'^build/(?P\d+)/target/(?P\d+)/packages/$', 'tpackage', name='tpackage'), - url(r'^build/(?P\d+)/configuration/$', 'configuration', name='configuration'), +urlpatterns = patterns('toastergui.views', + # landing page + url(r'^builds/$', 'builds', name='all-builds'), + # build info navigation + url(r'^build/(?P\d+)$', 'builddashboard', name="builddashboard"), + + url(r'^build/(?P\d+)/tasks/$', 'tasks', name='tasks'), + url(r'^build/(?P\d+)/task/(?P\d+)$', 'task', name='task'), + + url(r'^build/(?P\d+)/recipes/$', 'recipes', name='recipes'), + url(r'^build/(?P\d+)/recipe/(?P\d+)$', 'recipe', name='recipe'), + + url(r'^build/(?P\d+)/packages/$', 'bpackage', name='packages'), + url(r'^build/(?P\d+)/package/(?P\d+)$', 'bfile', name='package'), + + # images are known as targets in the internal model + url(r'^build/(?P\d+)/target/(?P\d+)$', 'target', name='target'), + url(r'^build/(?P\d+)/target/(?P\d+)/packages$', 'tpackage', name='targetpackages'), + + url(r'^build/(?P\d+)/configuration$', 'configuration', name='configuration'), + url(r'^build/(?P\d+)/buildtime$', 'buildtime', name='buildtime'), + url(r'^build/(?P\d+)/cpuusage$', 'cpuusage', name='cpuusage'), + url(r'^build/(?P\d+)/diskio$', 'diskio', name='diskio'), + + + # urls not linked from the dashboard url(r'^layers/$', 'layer', name='all-layers'), url(r'^layerversions/(?P\d+)/recipes/.*$', 'layer_versions_recipes', name='layer_versions_recipes'), + # default redirection url(r'^$', RedirectView.as_view( url= 'builds/')), ) diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py index 7cb9b42379..663e03dfd2 100644 --- a/bitbake/lib/toaster/toastergui/views.py +++ b/bitbake/lib/toaster/toastergui/views.py @@ -19,7 +19,7 @@ import operator from django.db.models import Q -from django.shortcuts import render +from django.shortcuts import render, redirect 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 @@ -35,6 +35,7 @@ def _build_page_range(paginator, index = 1): except EmptyPage: page = paginator.page(paginator.num_pages) + page.page_range = [page.number] crt_range = 0 for i in range(1,5): @@ -48,22 +49,124 @@ def _build_page_range(paginator, index = 1): break return page -@cache_control(no_store=True) -def build(request): - template = 'build.html' - logs = LogMessage.objects.all() - build_info = _build_page_range(Paginator(Build.objects.order_by("-id"), 10),request.GET.get('page', 1)) +def _verify_parameters(g, mandatory_parameters): + miss = [] + for mp in mandatory_parameters: + if not mp in g: + miss.append(mp) + if len(miss): + return miss + return None + +def _redirect_parameters(view, g, mandatory_parameters): + import urllib + from django.core.urlresolvers import reverse + url = reverse(view) + params = {} + for i in g: + params[i] = g[i] + for i in mandatory_parameters: + if not i in params: + params[i] = mandatory_parameters[i] + + return redirect(url + "?%s" % urllib.urlencode(params)) + - context = {'objects': build_info, 'logs': logs , - 'hideshowcols' : [ - {'name': 'Output', 'order':10}, - {'name': 'Log', 'order':11}, +# shows the "all builds" page +def builds(request): + template = 'build.html' + # 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. + mandatory_parameters = { 'count': 10, 'page' : 1}; + retval = _verify_parameters( request.GET, mandatory_parameters ) + if retval: + return _redirect_parameters( builds, request.GET, mandatory_parameters) + + # retrieve the objects that will be displayed in the table + build_info = _build_page_range(Paginator(Build.objects.exclude(outcome = Build.IN_PROGRESS).order_by("-id"), request.GET.get('count', 10)),request.GET.get('page', 1)) + + # build view-specific information; this is rendered specifically in the builds page + build_mru = Build.objects.order_by("-started_on")[:3] + for b in [ x for x in build_mru if x.outcome == Build.IN_PROGRESS ]: + tf = Task.objects.filter(build = b) + b.completeper = tf.exclude(order__isnull=True).count()*100/tf.count() + from django.utils import timezone + b.eta = timezone.now() + ((timezone.now() - b.started_on)*100/b.completeper) + + # 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, + 'tablecols' : [ + {'name': 'Target ', 'clclass': 'target',}, + {'name': 'Machine ', 'clclass': 'machine'}, + {'name': 'Completed on ', 'clclass': 'completed_on'}, + {'name': 'Failed tasks ', 'clclass': 'failed_tasks'}, + {'name': 'Errors ', 'clclass': 'errors_no'}, + {'name': 'Warnings', 'clclass': 'warnings_no'}, + {'name': 'Output ', 'clclass': 'output'}, + {'name': 'Started on ', 'clclass': 'started_on', 'hidden' : 1}, + {'name': 'Time ', 'clclass': 'time', 'hidden' : 1}, + {'name': 'Output', 'clclass': 'output'}, + {'name': 'Log', 'clclass': 'log', 'hidden': 1}, ]} return render(request, template, context) +# build dashboard for a single build, coming in as argument +def builddashboard(request, build_id): + template = "builddashboard.html" + if Build.objects.filter(pk=build_id).count() == 0 : + return redirect(builds) + context = { + 'build' : Build.objects.filter(pk=build_id)[0], + } + return render(request, template, context) + +def task(request, build_id, task_id): + template = "singletask.html" + if Build.objects.filter(pk=build_id).count() == 0 : + return redirect(builds) + context = { + 'build' : Build.objects.filter(pk=build_id)[0], + } + return render(request, template, context) + +def recipe(request, build_id, recipe_id): + template = "recipe.html" + if Recipe.objects.filter(pk=recipe_id).count() == 0 : + return redirect(builds) + context = { + 'build' : Build.objects.filter(pk=build_id)[0], + 'object' : Recipe.objects.filter(pk=recipe_id)[0], + } + return render(request, template, context) + +def package(request, build_id, package_id): + template = "singlepackage.html" + if Build.objects.filter(pk=build_id).count() == 0 : + return redirect(builds) + context = { + 'build' : Build.objects.filter(pk=build_id)[0], + } + return render(request, template, context) + +def target(request, build_id, target_id): + template = "target.html" + if Build.objects.filter(pk=build_id).count() == 0 : + return redirect(builds) + context = { + 'build' : Build.objects.filter(pk=build_id)[0], + } + return render(request, template, context) + + + def _find_task_revdep(task): tp = [] for p in Task_Dependency.objects.filter(depends_on=task): @@ -81,7 +184,7 @@ def _find_task_provider(task): return trc return None -def task(request, build_id): +def tasks(request, build_id): template = 'task.html' tasks = _build_page_range(Paginator(Task.objects.filter(build=build_id), 100),request.GET.get('page', 1)) @@ -94,12 +197,52 @@ def task(request, build_id): return render(request, template, context) +def recipes(request, build_id): + template = 'recipe.html' + + recipes = _build_page_range(Paginator(Recipe.objects.filter(build_recipe=build_id), 100),request.GET.get('page', 1)) + + context = {'build': Build.objects.filter(pk=build_id)[0], 'objects': recipes} + + return render(request, template, context) + + def configuration(request, build_id): template = 'configuration.html' variables = _build_page_range(Paginator(Variable.objects.filter(build=build_id), 50), request.GET.get('page', 1)) context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : variables} return render(request, template, context) +def buildtime(request, build_id): + template = "buildtime.html" + if Build.objects.filter(pk=build_id).count() == 0 : + return redirect(builds) + context = { + 'build' : Build.objects.filter(pk=build_id)[0], + } + return render(request, template, context) + +def cpuusage(request, build_id): + template = "cpuusage.html" + if Build.objects.filter(pk=build_id).count() == 0 : + return redirect(builds) + context = { + 'build' : Build.objects.filter(pk=build_id)[0], + } + return render(request, template, context) + +def diskio(request, build_id): + template = "diskio.html" + if Build.objects.filter(pk=build_id).count() == 0 : + return redirect(builds) + context = { + 'build' : Build.objects.filter(pk=build_id)[0], + } + return render(request, template, context) + + + + def bpackage(request, build_id): template = 'bpackage.html' packages = Package.objects.filter(build = build_id) @@ -227,8 +370,8 @@ def model_explorer(request, model_name): response_data['count'] = queryset.count() else: response_data['count'] = 0 - response_data['list'] = serializers.serialize('json', queryset) +# response_data = serializers.serialize('json', queryset) return HttpResponse(json.dumps(response_data), content_type='application/json') diff --git a/bitbake/lib/toaster/toastermain/settings.py b/bitbake/lib/toaster/toastermain/settings.py index b76218b959..679035e887 100644 --- a/bitbake/lib/toaster/toastermain/settings.py +++ b/bitbake/lib/toaster/toastermain/settings.py @@ -133,6 +133,15 @@ TEMPLATE_DIRS = ( # Don't forget to use absolute paths, not relative paths. ) +TEMPLATE_CONTEXT_PROCESSORS = ('django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.core.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + "django.core.context_processors.request") + INSTALLED_APPS = ( #'django.contrib.auth', #'django.contrib.contenttypes', @@ -144,6 +153,7 @@ INSTALLED_APPS = ( # 'django.contrib.admin', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', + 'django.contrib.humanize', 'orm', 'toastermain', 'toastergui', -- cgit v1.2.3-54-g00ecf
    {{package.name}} ({{package.filelist_bpackage.count}} files){{package.name}} ({{package.filelist_bpackage.count}} files) {{package.version}}-{{package.revision}} {%if package.recipe%}{{package.recipe.name}}{{package.package_name}}{%endif%}
    OutcomeStarted OnCompleted OnTargetMachineTimeErrorsWarningsOutputLogBitbake VersionBuild Name Outcome Target Machine Started on Completed on Failed tasks Errors Warnings Time Log Output
    {{build.get_outcome_display}}{{build.started_on}}{{build.completed_on}}{% for t in build.target_set.all %}{%if t.is_image %}{% endif %}{{t.target}}{% if t.is_image %}{% endif %}
    {% endfor %}
    {{build.machine}}{% time_difference build.started_on build.completed_on %}{{build.errors_no}}:{% if build.errors_no %}{% for error in logs %}{% if error.build == build %}{% if error.level == 2 %}

    {{error.message}}

    {% endif %}{% endif %}{% endfor %}{% else %}None{% endif %}
    {{build.warnings_no}}:{% if build.warnings_no %}{% for warning in logs %}{% if warning.build == build %}{% if warning.level == 1 %}

    {{warning.message}}

    {% endif %}{% endif %}{% endfor %}{% else %}None{% endif %}
    {% if build.outcome == 0 %}{% for t in build.target_set.all %}{% if t.is_image %}{{build.image_fstypes}}{% endif %}{% endfor %}{% endif %}{{build.cooker_log_path}}{{build.bitbake_version}}{{build.build_name}}{%if build.outcome == build.SUCCEEDED%}{%elif build.outcome == build.FAILED%}{%else%}{%endif%}{% for t in build.target_set.all %}{%if t.is_image %}{% endif %}{{t.target}}{% if t.is_image %}{% endif %}
    {% endfor %}
    {{build.machine}}{{build.started_on}}{{build.completed_on}}{% if build.errors_no %}{{build.errors_no}} error{{build.errors_no|pluralize}}{%endif%}{% if build.warnings_no %}{{build.warnings_no}} warning{{build.warnings_no|pluralize}}{%endif%}{{build|timespent}}{{build.log}}{% if build.outcome == 0 %}{% for t in build.target_set.all %}{% if t.is_image %}{{build.image_fstypes}}{% endif %}{% endfor %}{% endif %}
    Name{{variable.variable_value}}