From 2d78912bc62b7452433e3c785327d542449e3011 Mon Sep 17 00:00:00 2001 From: Alexandru DAMIAN Date: Wed, 14 Jan 2015 12:46:54 +0000 Subject: bitbake: toastergui: all builds page lists failed build requests This patch modifies the all builds page by splitting the page into two variants - the "interactive" (default) and "managed" mode versions. In the "managed" mode version, we display build requests instead of builds, including the failed build requests that have no build associated with them. [YOCTO #6671] (Bitbake rev: c5f5fb80308228585aa7ff9721352feb5ed9c961) Signed-off-by: Alexandru DAMIAN Signed-off-by: Richard Purdie --- .../toastergui/templates/managed_builds.html | 135 +++++ .../toastergui/templates/managed_mrb_section.html | 172 ++++++ .../toaster/toastergui/templatetags/projecttags.py | 19 + bitbake/lib/toaster/toastergui/views.py | 615 ++++++++++++++------- 4 files changed, 742 insertions(+), 199 deletions(-) create mode 100644 bitbake/lib/toaster/toastergui/templates/managed_builds.html create mode 100644 bitbake/lib/toaster/toastergui/templates/managed_mrb_section.html (limited to 'bitbake') diff --git a/bitbake/lib/toaster/toastergui/templates/managed_builds.html b/bitbake/lib/toaster/toastergui/templates/managed_builds.html new file mode 100644 index 0000000000..5944dc4747 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/managed_builds.html @@ -0,0 +1,135 @@ +{% extends "base.html" %} + +{% load static %} +{% load projecttags %} +{% load humanize %} + +{% block pagecontent %} +
+ + {% include "managed_mrb_section.html" %} + + + {% if 1 %} + + + {% if objects.paginator.count == 0 %} +
+
+
+ {% if request.GET.search %}{% endif %} + + +
+
+
+ + + {% else %} + {% include "basetable_top_buildprojects.html" %} + + {% for br in objects %}{% if br.build %} {% with build=br.build %} {# if we have a build, just display it #} + + {%if build.outcome == build.SUCCEEDED%}{%elif build.outcome == build.FAILED%}{%else%}{%endif%} + {% for t in build.target_set.all %} {{t.target}}
{% endfor %} + {{build.machine}} + {{build.started_on|date:"d/m/y H:i"}} + {{build.completed_on|date:"d/m/y H:i"}} + + {% query build.task_build outcome=4 order__gt=0 as exectask%} + {% if exectask.count == 1 %} + {{exectask.0.recipe.name}}.{{exectask.0.task_name}} + {% if MANAGED and build.project %} + + + + {% endif %} + {% elif exectask.count > 1%} + {{exectask.count}} task{{exectask.count|pluralize}} + {%endif%} + + + {% if build.errors_no %} + {{build.errors_no}} error{{build.errors_no|pluralize}} + {% if MANAGED and build.project %} + + + + {% endif %} + {%endif%} + + {% if build.warnings_no %}{{build.warnings_no}} warning{{build.warnings_no|pluralize}}{%endif%} + {{build.timespent|sectohms}} + {% if not MANAGED or not build.project %} + {{build.cooker_log_path}} + {% endif %} + + {% if build.outcome == build.SUCCEEDED %} + {{fstypes|get_dict_value:build.id}} + {% endif %} + + {% if MANAGED %} + + {% if build.project %} + {{build.project.name}} + {% endif %} + + {% endif %} + + + + {%endwith%} + {% else %} {# we don't have a build for this build request, mask the data with build request data #} + + + + + {% if buildrequest.state == buildrequest.REQ_FAILED %}{%else%}FIXME_build_request_state{%endif%} + + 1%}title="Targets: {%for target in br.brtarget_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{br.brtarget_set.all.0.target}} {%if br.brtarget_set.all.count > 1%}(+ {{br.brtarget_set.all.count|add:"-1"}}){%endif%} + + + {{br.machine}} + + + {{br.created|date:"d/m/y H:i"}} + + + {{br.updated|date:"d/m/y H:i"}} + + + {{br.brerror_set.all.0.errmsg|whitespace_slice:":32"}} + + + + + + + {{br.timespent.total_seconds|sectohms}} + + {# we have no output here #} + + + {{br.project.name}} + + + {%endif%} + {% endfor %} + + + {% include "basetable_bottom.html" %} + {% endif %} {# objects.paginator.count #} +{% endif %} {# empty #} +
+ +{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/managed_mrb_section.html b/bitbake/lib/toaster/toastergui/templates/managed_mrb_section.html new file mode 100644 index 0000000000..d4959a0b52 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/managed_mrb_section.html @@ -0,0 +1,172 @@ +{% load static %} +{% load projecttags %} +{% load humanize %} + + +{%if mru.count > 0%} + + +
+ {% for buildrequest in mru %}{% with build=buildrequest.build %} + + {% if build %} {# if we have a build, just display it #} + +
+ {% if MANAGED and build.project %} + {{build.project.name}} + {% endif %} + +
+ +
+ {% if build.completed_on|format_build_date %} + {{ build.completed_on|date:'d/m/y H:i' }} + {% else %} + {{ build.completed_on|date:'H:i' }} + {% endif %} +
+ {%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|sectohms }} + + {% if MANAGED and build.project %} + Run again + {% endif %} +
+ {%endif%} + {%if build.outcome == build.IN_PROGRESS %} +
+
+
+
+
+
ETA: in {{build.eta|naturaltime}}
+ {%endif%} +
+
+ + {% else %} {# we use the project's page recent build design #} + +
+
+ + + {% if buildrequest.state == buildrequest.REQ_FAILED %} +
+ 1%}title="Targets: {%for target in buildrequest.brtarget_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{buildrequest.brtarget_set.all.0.target}} {%if buildrequest.brtarget_set.all.count > 1%}(+ {{buildrequest.brtarget_set.all.count|add:"-1"}}){%endif%} +
+
+
+
+ {% for e in buildrequest.brerror_set.all|slice:":3" %} +
+
{{e.errmsg|whitespace_slice:":150"}}
+
+ {% endfor %} +
+ + {% elif buildrequest.state == buildrequest.REQ_QUEUED %} + +
+ + 1%}title="Targets: {%for target in buildrequest.brtarget_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{buildrequest.brtarget_set.all.0.target}} {%if buildrequest.brtarget_set.all.count > 1%}(+ {{buildrequest.brtarget_set.all.count|add:"-1"}}){%endif%} +
+
Build queued + +
+ + {% elif buildrequest.state == buildrequest.REQ_CREATED %} + +
+ 1%}title="Targets: {%for target in buildrequest.brtarget_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{buildrequest.brtarget_set.all.0.target}} {%if buildrequest.brtarget_set.all.count > 1%}(+ {{buildrequest.brtarget_set.all.count|add:"-1"}}){%endif%} +
+
+ Creating build +
+ + {% elif buildrequest.state == buildrequest.REQ_INPROGRESS %} + +
+ 1%}title="Targets: {%for target in buildrequest.brtarget_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{buildrequest.brtarget_set.all.0.target}} {%if buildrequest.brtarget_set.all.count > 1%}(+ {{buildrequest.brtarget_set.all.count|add:"-1"}}){%endif%} +
+
+ Checking out layers +
+ {% else %} + +
FIXME!
+ + {% endif %} +
+
+
+
+ + + + {% endif %} {# this ends the build request most recent build section #} + +{%endwith%}{% endfor %} +
+ + + +{%endif%} + diff --git a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py index f564edfe49..276c6eb098 100644 --- a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py +++ b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py @@ -65,6 +65,25 @@ def query(qs, **kwargs): """ return qs.filter(**kwargs) + +@register.filter("whitespace_slice") +def whitespace_space_filter(value, arg): + try: + bits = [] + for x in arg.split(":"): + if len(x) == 0: + bits.append(None) + else: + # convert numeric value to the first whitespace after + first_whitespace = value.find(" ", int(x)) + if first_whitespace == -1: + bits.append(int(x)) + else: + bits.append(first_whitespace) + return value[slice(*bits)] + except (ValueError, TypeError): + raise + @register.filter def divide(value, arg): if int(arg) == 0: diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py index e414b66480..e8e4927b7e 100755 --- a/bitbake/lib/toaster/toastergui/views.py +++ b/bitbake/lib/toaster/toastergui/views.py @@ -255,197 +255,6 @@ def _save_parameters_cookies(response, pagesize, orderby, request): return response - -# 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. - (pagesize, orderby) = _get_parameters_values(request, 10, 'completed_on:-') - mandatory_parameters = { 'count': pagesize, 'page' : 1, 'orderby' : orderby } - retval = _verify_parameters( request.GET, mandatory_parameters ) - if retval: - return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters) - - # 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) - queryset_all = Build.objects.exclude(outcome = Build.IN_PROGRESS) - 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 = Build.objects.order_by("-started_on")[:3] - - # 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' : 'completed_on:-', - 'fstypes' : fstypes_map, - 'search_term' : search_term, - 'total_count' : queryset_with_search.count(), - # 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': 'Target', # default column, disabled box, with just the name in the list - 'qhelp': "This is the build target or build targets (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"), - 'filter' : {'class' : 'started_on', - 'label': 'Show:', - 'options' : [ - ("Today's builds" , 'started_on__gte:'+timezone.now().strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=timezone.now()).count()), - ("Yesterday's builds", 'started_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=(timezone.now()-timedelta(hours=24))).count()), - ("This week's builds", 'started_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=(timezone.now()-timedelta(days=7))).count()), - ] - } - }, - {'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:'+timezone.now().strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=timezone.now()).count()), - ("Yesterday's builds", 'completed_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=(timezone.now()-timedelta(hours=24))).count()), - ("This week's builds", 'completed_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=(timezone.now()-timedelta(days=7))).count()), - ] - } - }, - {'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)", - '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)", - '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", - '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 /build/tmp/deploy/images/ directory", - # TODO: compute image fstypes from Target_Image_File - }, - ] - } - - if not toastermain.settings.MANAGED: - context['tablecols'].insert(-2, - {'name': 'Log1', - 'dclass': "span4", - 'qhelp': "Path to the build main log file", - 'clclass': 'log', 'hidden': 1, - 'orderfield': _get_toggle_order(request, "cooker_log_path"), - 'ordericon':_get_toggle_order_icon(request, "cooker_log_path"), - 'orderkey' : 'cooker_log_path', - } - ) - - - if toastermain.settings.MANAGED: - context['tablecols'].append( - {'name': 'Project', 'clclass': 'project', - 'filter': {'class': 'project', - 'label': 'Project:', - 'options': map(lambda x: (x.name,'',x.build_set.filter(outcome__lt=Build.IN_PROGRESS).count()), Project.objects.all()), - - } - } - ) - - - response = render(request, template, context) - _save_parameters_cookies(response, pagesize, orderby, request) - return response - - ## # build dashboard for a single build, coming in as argument # Each build may contain multiple targets and each target @@ -1895,6 +1704,221 @@ if toastermain.settings.MANAGED: del request.session['project_id'] return ret + + # shows the "all builds" page for managed mode; it displays build requests (at least started!) instead of actual builds + def builds(request): + template = 'managed_builds.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. + + # ATTN: we use here the ordering parameters for interactive mode; the translation for BuildRequest fields will happen below + (pagesize, orderby) = _get_parameters_values(request, 10, 'completed_on:-') + mandatory_parameters = { 'count': pagesize, 'page' : 1, 'orderby' : orderby } + retval = _verify_parameters( request.GET, mandatory_parameters ) + if retval: + return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters) + + # translate interactive mode ordering to managed mode ordering + ordering_params = orderby.split(":") + if ordering_params[0] == "completed_on": + ordering_params[0] = "updated" + if ordering_params[0] == "started_on": + ordering_params = "created" + + request.GET = request.GET.copy() # get a mutable copy of the GET QueryDict + request.GET['orderby'] = ":".join(ordering_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, BuildRequest) + # we don't display in-progress or deleted builds + queryset_all = BuildRequest.objects.exclude(state__lte = BuildRequest.REQ_INPROGRESS).exclude(state=BuildRequest.REQ_DELETED) + queryset_with_search = _get_queryset(BuildRequest, queryset_all, None, search_term, ordering_string, '-updated') + queryset = _get_queryset(BuildRequest, queryset_all, filter_string, search_term, ordering_string, '-updated') + + # 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) + # most recent build is like projects' most recent builds, but across all projects + build_mru = BuildRequest.objects.all() + build_mru = list(build_mru.filter(Q(state__lt=BuildRequest.REQ_COMPLETED) or Q(state=BuildRequest.REQ_DELETED)).order_by("-pk")) + list(build_mru.filter(state__in=[BuildRequest.REQ_COMPLETED, BuildRequest.REQ_FAILED]).order_by("-pk")[:3]) + + fstypes_map = {}; + for build_request in build_info: + # set display variables for build request + build_request.machine = build_request.brvariable_set.get(name="MACHINE").value + build_request.timespent = build_request.updated - build_request.created + + # set up list of fstypes for each build + if build_request.build is None: + continue + targets = Target.objects.filter( build_id = build_request.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_request.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' : 'updated:-', + 'fstypes' : fstypes_map, + 'search_term' : search_term, + 'total_count' : queryset_with_search.count(), + # 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, "state"), # adds ordering by the field value; default ascending unless clicked from ascending into descending + 'ordericon':_get_toggle_order_icon(request, "state"), + # 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', 'state:' + str(BuildRequest.REQ_COMPLETED), queryset_with_search.filter(state=str(BuildRequest.REQ_COMPLETED)).count()), # this is the field search expression + ('Failed builds', 'state:'+ str(BuildRequest.REQ_FAILED), queryset_with_search.filter(state=str(BuildRequest.REQ_FAILED)).count()), + ] + } + }, + {'name': 'Target', # default column, disabled box, with just the name in the list + 'qhelp': "This is the build target or build targets (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, "created", True), + 'ordericon':_get_toggle_order_icon(request, "created"), + 'filter' : {'class' : 'created', + 'label': 'Show:', + 'options' : [ + ("Today's builds" , 'created__gte:'+timezone.now().strftime("%Y-%m-%d"), queryset_with_search.filter(created__gte=timezone.now()).count()), + ("Yesterday's builds", 'created__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_with_search.filter(created__gte=(timezone.now()-timedelta(hours=24))).count()), + ("This week's builds", 'created__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_with_search.filter(created__gte=(timezone.now()-timedelta(days=7))).count()), + ] + } + }, + {'name': 'Completed on', + 'qhelp': "The date and time the build finished", + 'orderfield': _get_toggle_order(request, "updated", True), + 'ordericon':_get_toggle_order_icon(request, "updated"), + 'orderkey' : 'updated', + 'filter' : {'class' : 'updated', + 'label': 'Show:', + 'options' : [ + ("Today's builds", 'updated__gte:'+timezone.now().strftime("%Y-%m-%d"), queryset_with_search.filter(updated__gte=timezone.now()).count()), + ("Yesterday's builds", 'updated__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_with_search.filter(updated__gte=(timezone.now()-timedelta(hours=24))).count()), + ("This week's builds", 'updated__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_with_search.filter(updated__gte=(timezone.now()-timedelta(days=7))).count()), + ] + } + }, + {'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' : [ + ('BuildRequests with failed tasks', 'build__task_build__outcome:4', queryset_with_search.filter(build__task_build__outcome=4).count()), + ('BuildRequests without failed tasks', 'build__task_build__outcome:NOT4', queryset_with_search.filter(~Q(build__task_build__outcome=4)).count()), + ] + } + }, + {'name': 'Errors', 'clclass': 'errors_no', + 'qhelp': "How many errors were encountered during the build (if any)", + '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' : [ + ('BuildRequests with errors', 'errors_no__gte:1', queryset_with_search.filter(build__errors_no__gte=1).count()), + ('BuildRequests without errors', 'errors_no:0', queryset_with_search.filter(build__errors_no=0).count()), + ] + } + }, + {'name': 'Warnings', 'clclass': 'warnings_no', + 'qhelp': "How many warnings were encountered during the build (if any)", + 'orderfield': _get_toggle_order(request, "build__warnings_no", True), + 'ordericon':_get_toggle_order_icon(request, "build__warnings_no"), + 'orderkey' : 'build__warnings_no', + 'filter' : {'class' : 'build__warnings_no', + 'label': 'Show:', + 'options' : [ + ('BuildRequests with warnings','build__warnings_no__gte:1', queryset_with_search.filter(build__warnings_no__gte=1).count()), + ('BuildRequests without warnings','build__warnings_no:0', queryset_with_search.filter(build__warnings_no=0).count()), + ] + } + }, + {'name': 'Time', 'clclass': 'time', 'hidden' : 1, + 'qhelp': "How long it took the build to finish", + '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 /build/tmp/deploy/images/ directory", + # TODO: compute image fstypes from Target_Image_File + }, + ] + } + + if not toastermain.settings.MANAGED: + context['tablecols'].insert(-2, + {'name': 'Log1', + 'dclass': "span4", + 'qhelp': "Path to the build main log file", + 'clclass': 'log', 'hidden': 1, + 'orderfield': _get_toggle_order(request, "cooker_log_path"), + 'ordericon':_get_toggle_order_icon(request, "cooker_log_path"), + 'orderkey' : 'cooker_log_path', + } + ) + + + if toastermain.settings.MANAGED: + context['tablecols'].append( + {'name': 'Project', 'clclass': 'project', + 'filter': {'class': 'project', + 'label': 'Project:', + 'options': map(lambda x: (x.name,'',x.build_set.filter(outcome__lt=BuildRequest.REQ_INPROGRESS).count()), Project.objects.all()), + + } + } + ) + + + response = render(request, template, context) + _save_parameters_cookies(response, pagesize, orderby, request) + return response + + + + # new project def newproject(request): template = "newproject.html" @@ -2819,21 +2843,21 @@ if toastermain.settings.MANAGED: '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()), + ('Builds with failed tasks', 'build__task_build__outcome:4', queryset_with_search.filter(build__task_build__outcome=4).count()), + ('Builds without failed tasks', 'build__task_build__outcome:NOT4', queryset_with_search.filter(~Q(build__task_build__outcome=4)).count()), ] } }, {'name': 'Errors', 'clclass': 'errors_no', 'qhelp': "How many errors were encountered during the build (if any)", - 'orderfield': _get_toggle_order(request, "errors_no", True), - 'ordericon':_get_toggle_order_icon(request, "errors_no"), - 'orderkey' : 'errors_no', - 'filter' : {'class' : 'errors_no', + 'orderfield': _get_toggle_order(request, "build__errors_no", True), + 'ordericon':_get_toggle_order_icon(request, "build__errors_no"), + 'orderkey' : 'build__errors_no', + 'filter' : {'class' : 'build__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()), + ('Builds with errors', 'build__errors_no__gte:1', queryset_with_search.filter(build__errors_no__gte=1).count()), + ('Builds without errors', 'build__errors_no:0', queryset_with_search.filter(build__errors_no=0).count()), ] } }, @@ -3007,6 +3031,199 @@ else: "DEBUG" : toastermain.settings.DEBUG } + + # shows the "all builds" page for interactive mode; this is the old code, simply moved + 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. + (pagesize, orderby) = _get_parameters_values(request, 10, 'completed_on:-') + mandatory_parameters = { 'count': pagesize, 'page' : 1, 'orderby' : orderby } + retval = _verify_parameters( request.GET, mandatory_parameters ) + if retval: + return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters) + + # 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) + queryset_all = Build.objects.exclude(outcome = Build.IN_PROGRESS) + 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 = Build.objects.order_by("-started_on")[:3] + + # 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' : 'completed_on:-', + 'fstypes' : fstypes_map, + 'search_term' : search_term, + 'total_count' : queryset_with_search.count(), + # 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': 'Target', # default column, disabled box, with just the name in the list + 'qhelp': "This is the build target or build targets (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"), + 'filter' : {'class' : 'started_on', + 'label': 'Show:', + 'options' : [ + ("Today's builds" , 'started_on__gte:'+timezone.now().strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=timezone.now()).count()), + ("Yesterday's builds", 'started_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=(timezone.now()-timedelta(hours=24))).count()), + ("This week's builds", 'started_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=(timezone.now()-timedelta(days=7))).count()), + ] + } + }, + {'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:'+timezone.now().strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=timezone.now()).count()), + ("Yesterday's builds", 'completed_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=(timezone.now()-timedelta(hours=24))).count()), + ("This week's builds", 'completed_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=(timezone.now()-timedelta(days=7))).count()), + ] + } + }, + {'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)", + '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)", + '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", + '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 /build/tmp/deploy/images/ directory", + # TODO: compute image fstypes from Target_Image_File + }, + ] + } + + if not toastermain.settings.MANAGED: + context['tablecols'].insert(-2, + {'name': 'Log1', + 'dclass': "span4", + 'qhelp': "Path to the build main log file", + 'clclass': 'log', 'hidden': 1, + 'orderfield': _get_toggle_order(request, "cooker_log_path"), + 'ordericon':_get_toggle_order_icon(request, "cooker_log_path"), + 'orderkey' : 'cooker_log_path', + } + ) + + + if toastermain.settings.MANAGED: + context['tablecols'].append( + {'name': 'Project', 'clclass': 'project', + 'filter': {'class': 'project', + 'label': 'Project:', + 'options': map(lambda x: (x.name,'',x.build_set.filter(outcome__lt=Build.IN_PROGRESS).count()), Project.objects.all()), + + } + } + ) + + + response = render(request, template, context) + _save_parameters_cookies(response, pagesize, orderby, request) + return response + + + + def newproject(request): raise Exception("page not available in interactive mode") -- cgit v1.2.3-54-g00ecf