From ca485075fb81281cc7dd1e9a2f2be723ef1ddd36 Mon Sep 17 00:00:00 2001 From: Alexandru DAMIAN Date: Wed, 11 Dec 2013 16:42:34 +0000 Subject: [PATCH] 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 diff --git a/bitbake/lib/toaster/toastergui/templates/base.html b/bitbake/lib/toaster/toastergui/templates/base.html index d58cbea..3508962 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 0000000..2a6f084 --- /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 0000000..b9277b4 --- /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 67fc65c..3329dda 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 4fa87d5..27ce1cc 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 0000000..7c58cc0 --- /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 0000000..ea84ae7 --- /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 521620f..e390a95 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 0000000..02f07b7 --- /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 0000000..c5cef6f --- /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 d7f57eb..87c69b8 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 0000000..f2d0ad4 --- /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 de965ab..6af2c51 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 0c0d804..5f60379 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 b84c95f..f531eb0 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 7cb9b42..663e03d 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 b76218b..679035e 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', -- 2.7.4
    {{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}}