summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bitbake/lib/toaster/orm/models.py32
-rw-r--r--bitbake/lib/toaster/toastergui/querysetfilter.py24
-rw-r--r--bitbake/lib/toaster/toastergui/tables.py343
-rw-r--r--bitbake/lib/toaster/toastergui/templates/builds-toastertable.html62
-rw-r--r--bitbake/lib/toaster/toastergui/urls.py5
-rwxr-xr-xbitbake/lib/toaster/toastergui/views.py32
-rw-r--r--bitbake/lib/toaster/toastergui/widgets.py16
7 files changed, 434 insertions, 80 deletions
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index b7975ef865..3dc4d6d891 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -447,6 +447,12 @@ class Build(models.Model):
447 return Build.BUILD_OUTCOME[int(self.outcome)][1] 447 return Build.BUILD_OUTCOME[int(self.outcome)][1]
448 448
449 @property 449 @property
450 def failed_tasks(self):
451 """ Get failed tasks for the build """
452 tasks = self.task_build.all()
453 return tasks.filter(order__gt=0, outcome=Task.OUTCOME_FAILED)
454
455 @property
450 def errors(self): 456 def errors(self):
451 return (self.logmessage_set.filter(level=LogMessage.ERROR) | 457 return (self.logmessage_set.filter(level=LogMessage.ERROR) |
452 self.logmessage_set.filter(level=LogMessage.EXCEPTION) | 458 self.logmessage_set.filter(level=LogMessage.EXCEPTION) |
@@ -457,8 +463,32 @@ class Build(models.Model):
457 return self.logmessage_set.filter(level=LogMessage.WARNING) 463 return self.logmessage_set.filter(level=LogMessage.WARNING)
458 464
459 @property 465 @property
466 def timespent(self):
467 return self.completed_on - self.started_on
468
469 @property
460 def timespent_seconds(self): 470 def timespent_seconds(self):
461 return (self.completed_on - self.started_on).total_seconds() 471 return self.timespent.total_seconds()
472
473 @property
474 def target_labels(self):
475 """
476 Sorted (a-z) "target1:task, target2, target3" etc. string for all
477 targets in this build
478 """
479 targets = self.target_set.all()
480 target_labels = []
481 target_label = None
482
483 for target in targets:
484 target_label = target.target
485 if target.task:
486 target_label = target_label + ':' + target.task
487 target_labels.append(target_label)
488
489 target_labels.sort()
490
491 return target_labels
462 492
463 def get_current_status(self): 493 def get_current_status(self):
464 """ 494 """
diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py b/bitbake/lib/toaster/toastergui/querysetfilter.py
new file mode 100644
index 0000000000..62297e9b89
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/querysetfilter.py
@@ -0,0 +1,24 @@
1class QuerysetFilter(object):
2 """ Filter for a queryset """
3
4 def __init__(self, criteria=None):
5 if criteria:
6 self.set_criteria(criteria)
7
8 def set_criteria(self, criteria):
9 """
10 criteria is an instance of django.db.models.Q;
11 see https://docs.djangoproject.com/en/1.9/ref/models/querysets/#q-objects
12 """
13 self.criteria = criteria
14
15 def filter(self, queryset):
16 """
17 Filter queryset according to the criteria for this filter,
18 returning the filtered queryset
19 """
20 return queryset.filter(self.criteria)
21
22 def count(self, queryset):
23 """ Returns a count of the elements in the filtered queryset """
24 return self.filter(queryset).count()
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 2e3c8a6956..116cff3f43 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -20,29 +20,18 @@
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 21
22from toastergui.widgets import ToasterTable 22from toastergui.widgets import ToasterTable
23from toastergui.querysetfilter import QuerysetFilter
23from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project 24from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
24from orm.models import CustomImageRecipe, Package, Build 25from orm.models import CustomImageRecipe, Package, Build, LogMessage, Task
25from django.db.models import Q, Max, Count 26from django.db.models import Q, Max, Count
26from django.conf.urls import url 27from django.conf.urls import url
27from django.core.urlresolvers import reverse 28from django.core.urlresolvers import reverse
28from django.views.generic import TemplateView 29from django.views.generic import TemplateView
29 30
30class ProjectFiltersMixin(object): 31class ProjectFilters(object):
31 """Common mixin for recipe, machine in project filters""" 32 def __init__(self, project_layers):
32 33 self.in_project = QuerysetFilter(Q(layer_version__in=project_layers))
33 def filter_in_project(self, count_only=False): 34 self.not_in_project = QuerysetFilter(~Q(layer_version__in=project_layers))
34 query = self.queryset.filter(layer_version__in=self.project_layers)
35 if count_only:
36 return query.count()
37
38 self.queryset = query
39
40 def filter_not_in_project(self, count_only=False):
41 query = self.queryset.exclude(layer_version__in=self.project_layers)
42 if count_only:
43 return query.count()
44
45 self.queryset = query
46 35
47class LayersTable(ToasterTable): 36class LayersTable(ToasterTable):
48 """Table of layers in Toaster""" 37 """Table of layers in Toaster"""
@@ -60,34 +49,21 @@ class LayersTable(ToasterTable):
60 49
61 return context 50 return context
62 51
63
64 def setup_filters(self, *args, **kwargs): 52 def setup_filters(self, *args, **kwargs):
65 project = Project.objects.get(pk=kwargs['pid']) 53 project = Project.objects.get(pk=kwargs['pid'])
66 self.project_layers = ProjectLayer.objects.filter(project=project) 54 self.project_layers = ProjectLayer.objects.filter(project=project)
67 55
56 criteria = Q(projectlayer__in=self.project_layers)
57 in_project_filter = QuerysetFilter(criteria)
58 not_in_project_filter = QuerysetFilter(~criteria)
68 59
69 self.add_filter(title="Filter by project layers", 60 self.add_filter(title="Filter by project layers",
70 name="in_current_project", 61 name="in_current_project",
71 filter_actions=[ 62 filter_actions=[
72 self.make_filter_action("in_project", "Layers added to this project", self.filter_in_project), 63 self.make_filter_action("in_project", "Layers added to this project", in_project_filter),
73 self.make_filter_action("not_in_project", "Layers not added to this project", self.filter_not_in_project) 64 self.make_filter_action("not_in_project", "Layers not added to this project", not_in_project_filter)
74 ]) 65 ])
75 66
76 def filter_in_project(self, count_only=False):
77 query = self.queryset.filter(projectlayer__in=self.project_layers)
78 if count_only:
79 return query.count()
80
81 self.queryset = query
82
83 def filter_not_in_project(self, count_only=False):
84 query = self.queryset.exclude(projectlayer__in=self.project_layers)
85 if count_only:
86 return query.count()
87
88 self.queryset = query
89
90
91 def setup_queryset(self, *args, **kwargs): 67 def setup_queryset(self, *args, **kwargs):
92 prj = Project.objects.get(pk = kwargs['pid']) 68 prj = Project.objects.get(pk = kwargs['pid'])
93 compatible_layers = prj.get_all_compatible_layer_versions() 69 compatible_layers = prj.get_all_compatible_layer_versions()
@@ -204,7 +180,7 @@ class LayersTable(ToasterTable):
204 computation = lambda x: x.layer.name) 180 computation = lambda x: x.layer.name)
205 181
206 182
207class MachinesTable(ToasterTable, ProjectFiltersMixin): 183class MachinesTable(ToasterTable):
208 """Table of Machines in Toaster""" 184 """Table of Machines in Toaster"""
209 185
210 def __init__(self, *args, **kwargs): 186 def __init__(self, *args, **kwargs):
@@ -221,11 +197,13 @@ class MachinesTable(ToasterTable, ProjectFiltersMixin):
221 def setup_filters(self, *args, **kwargs): 197 def setup_filters(self, *args, **kwargs):
222 project = Project.objects.get(pk=kwargs['pid']) 198 project = Project.objects.get(pk=kwargs['pid'])
223 199
200 project_filters = ProjectFilters(self.project_layers)
201
224 self.add_filter(title="Filter by project machines", 202 self.add_filter(title="Filter by project machines",
225 name="in_current_project", 203 name="in_current_project",
226 filter_actions=[ 204 filter_actions=[
227 self.make_filter_action("in_project", "Machines provided by layers added to this project", self.filter_in_project), 205 self.make_filter_action("in_project", "Machines provided by layers added to this project", project_filters.in_project),
228 self.make_filter_action("not_in_project", "Machines provided by layers not added to this project", self.filter_not_in_project) 206 self.make_filter_action("not_in_project", "Machines provided by layers not added to this project", project_filters.not_in_project)
229 ]) 207 ])
230 208
231 def setup_queryset(self, *args, **kwargs): 209 def setup_queryset(self, *args, **kwargs):
@@ -313,7 +291,7 @@ class LayerMachinesTable(MachinesTable):
313 static_data_template=select_btn_template) 291 static_data_template=select_btn_template)
314 292
315 293
316class RecipesTable(ToasterTable, ProjectFiltersMixin): 294class RecipesTable(ToasterTable):
317 """Table of All Recipes in Toaster""" 295 """Table of All Recipes in Toaster"""
318 296
319 def __init__(self, *args, **kwargs): 297 def __init__(self, *args, **kwargs):
@@ -338,11 +316,13 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin):
338 return context 316 return context
339 317
340 def setup_filters(self, *args, **kwargs): 318 def setup_filters(self, *args, **kwargs):
319 project_filters = ProjectFilters(self.project_layers)
320
341 self.add_filter(title="Filter by project recipes", 321 self.add_filter(title="Filter by project recipes",
342 name="in_current_project", 322 name="in_current_project",
343 filter_actions=[ 323 filter_actions=[
344 self.make_filter_action("in_project", "Recipes provided by layers added to this project", self.filter_in_project), 324 self.make_filter_action("in_project", "Recipes provided by layers added to this project", project_filters.in_project),
345 self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", self.filter_not_in_project) 325 self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", project_filters.not_in_project)
346 ]) 326 ])
347 327
348 def setup_queryset(self, *args, **kwargs): 328 def setup_queryset(self, *args, **kwargs):
@@ -853,3 +833,284 @@ class ProjectsTable(ToasterTable):
853 orderable=False, 833 orderable=False,
854 static_data_name='image_files', 834 static_data_name='image_files',
855 static_data_template=image_files_template) 835 static_data_template=image_files_template)
836
837class BuildsTable(ToasterTable):
838 """Table of builds in Toaster"""
839
840 def __init__(self, *args, **kwargs):
841 super(BuildsTable, self).__init__(*args, **kwargs)
842 self.default_orderby = '-completed_on'
843 self.title = 'All builds'
844 self.static_context_extra['Build'] = Build
845 self.static_context_extra['Task'] = Task
846
847 def get_context_data(self, **kwargs):
848 return super(BuildsTable, self).get_context_data(**kwargs)
849
850 def setup_queryset(self, *args, **kwargs):
851 queryset = Build.objects.all()
852
853 # don't include in progress builds
854 queryset = queryset.exclude(outcome=Build.IN_PROGRESS)
855
856 # sort
857 queryset = queryset.order_by(self.default_orderby)
858
859 # annotate with number of ERROR and EXCEPTION log messages
860 queryset = queryset.annotate(
861 errors_no = Count(
862 'logmessage',
863 only = Q(logmessage__level=LogMessage.ERROR) |
864 Q(logmessage__level=LogMessage.EXCEPTION)
865 )
866 )
867
868 # annotate with number of WARNING log messages
869 queryset = queryset.annotate(
870 warnings_no = Count(
871 'logmessage',
872 only = Q(logmessage__level=LogMessage.WARNING)
873 )
874 )
875
876 self.queryset = queryset
877
878 def setup_columns(self, *args, **kwargs):
879 outcome_template = '''
880 <a href="{% url "builddashboard" data.id %}">
881 {% if data.outcome == data.SUCCEEDED %}
882 <i class="icon-ok-sign success"></i>
883 {% elif data.outcome == data.FAILED %}
884 <i class="icon-minus-sign error"></i>
885 {% endif %}
886 </a>
887
888 {% if data.cooker_log_path %}
889 &nbsp;
890 <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
891 <i class="icon-download-alt" title="Download build log"></i>
892 </a>
893 {% endif %}
894 '''
895
896 recipe_template = '''
897 {% for target_label in data.target_labels %}
898 <a href="{% url "builddashboard" data.id %}">
899 {{target_label}}
900 </a>
901 <br />
902 {% endfor %}
903 '''
904
905 machine_template = '''
906 <a href="{% url "builddashboard" data.id %}">
907 {{data.machine}}
908 </a>
909 '''
910
911 started_on_template = '''
912 <a href="{% url "builddashboard" data.id %}">
913 {{data.started_on | date:"d/m/y H:i"}}
914 </a>
915 '''
916
917 completed_on_template = '''
918 <a href="{% url "builddashboard" data.id %}">
919 {{data.completed_on | date:"d/m/y H:i"}}
920 </a>
921 '''
922
923 failed_tasks_template = '''
924 {% if data.failed_tasks.count == 1 %}
925 <a href="{% url "task" data.id data.failed_tasks.0.id %}">
926 <span class="error">
927 {{data.failed_tasks.0.recipe.name}}.{{data.failed_tasks.0.task_name}}
928 </span>
929 </a>
930 <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}">
931 <i class="icon-download-alt"
932 data-original-title="Download task log file">
933 </i>
934 </a>
935 {% elif data.failed_tasks.count > 1 %}
936 <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}">
937 <span class="error">{{data.failed_tasks.count}} tasks</span>
938 </a>
939 {% endif %}
940 '''
941
942 errors_template = '''
943 {% if data.errors.count %}
944 <a class="errors.count error" href="{% url "builddashboard" data.id %}#errors">
945 {{data.errors.count}} error{{data.errors.count|pluralize}}
946 </a>
947 {% endif %}
948 '''
949
950 warnings_template = '''
951 {% if data.warnings.count %}
952 <a class="warnings.count warning" href="{% url "builddashboard" data.id %}#warnings">
953 {{data.warnings.count}} warning{{data.warnings.count|pluralize}}
954 </a>
955 {% endif %}
956 '''
957
958 time_template = '''
959 {% load projecttags %}
960 <a href="{% url "buildtime" data.id %}">
961 {{data.timespent_seconds | sectohms}}
962 </a>
963 '''
964
965 image_files_template = '''
966 {% if data.outcome == extra.Build.SUCCEEDED %}
967 <a href="{% url "builddashboard" data.id %}#images">
968 {{data.get_image_file_extensions}}
969 </a>
970 {% endif %}
971 '''
972
973 project_template = '''
974 {% load project_url_tag %}
975 <a href="{% project_url data.project %}">
976 {{data.project.name}}
977 </a>
978 {% if data.project.is_default %}
979 <i class="icon-question-sign get-help hover-help" title=""
980 data-original-title="This project shows information about
981 the builds you start from the command line while Toaster is
982 running" style="visibility: hidden;"></i>
983 {% endif %}
984 '''
985
986 self.add_column(title='Outcome',
987 help_text='Final state of the build (successful \
988 or failed)',
989 hideable=False,
990 orderable=True,
991 filter_name='outcome_filter',
992 static_data_name='outcome',
993 static_data_template=outcome_template)
994
995 self.add_column(title='Recipe',
996 help_text='What was built (i.e. one or more recipes \
997 or image recipes)',
998 hideable=False,
999 orderable=False,
1000 static_data_name='target',
1001 static_data_template=recipe_template)
1002
1003 self.add_column(title='Machine',
1004 help_text='Hardware for which you are building a \
1005 recipe or image recipe',
1006 hideable=False,
1007 orderable=True,
1008 static_data_name='machine',
1009 static_data_template=machine_template)
1010
1011 self.add_column(title='Started on',
1012 help_text='The date and time when the build started',
1013 hideable=True,
1014 orderable=True,
1015 static_data_name='started_on',
1016 static_data_template=started_on_template)
1017
1018 self.add_column(title='Completed on',
1019 help_text='The date and time when the build finished',
1020 hideable=False,
1021 orderable=True,
1022 static_data_name='completed_on',
1023 static_data_template=completed_on_template)
1024
1025 self.add_column(title='Failed tasks',
1026 help_text='The number of tasks which failed during \
1027 the build',
1028 hideable=True,
1029 orderable=False,
1030 filter_name='failed_tasks_filter',
1031 static_data_name='failed_tasks',
1032 static_data_template=failed_tasks_template)
1033
1034 self.add_column(title='Errors',
1035 help_text='The number of errors encountered during \
1036 the build (if any)',
1037 hideable=True,
1038 orderable=False,
1039 static_data_name='errors',
1040 static_data_template=errors_template)
1041
1042 self.add_column(title='Warnings',
1043 help_text='The number of warnings encountered during \
1044 the build (if any)',
1045 hideable=True,
1046 orderable=False,
1047 static_data_name='warnings',
1048 static_data_template=warnings_template)
1049
1050 self.add_column(title='Time',
1051 help_text='How long the build took to finish',
1052 hideable=False,
1053 orderable=False,
1054 static_data_name='time',
1055 static_data_template=time_template)
1056
1057 self.add_column(title='Image files',
1058 help_text='The root file system types produced by \
1059 the build',
1060 hideable=True,
1061 orderable=False,
1062 static_data_name='image_files',
1063 static_data_template=image_files_template)
1064
1065 self.add_column(title='Project',
1066 hideable=True,
1067 orderable=False,
1068 static_data_name='project-name',
1069 static_data_template=project_template)
1070
1071 def setup_filters(self, *args, **kwargs):
1072 # outcomes
1073 filter_only_successful_builds = QuerysetFilter(Q(outcome=Build.SUCCEEDED))
1074 successful_builds_filter = self.make_filter_action(
1075 'successful_builds',
1076 'Successful builds',
1077 filter_only_successful_builds
1078 )
1079
1080 filter_only_failed_builds = QuerysetFilter(Q(outcome=Build.FAILED))
1081 failed_builds_filter = self.make_filter_action(
1082 'failed_builds',
1083 'Failed builds',
1084 filter_only_failed_builds
1085 )
1086
1087 self.add_filter(title='Filter builds by outcome',
1088 name='outcome_filter',
1089 filter_actions = [
1090 successful_builds_filter,
1091 failed_builds_filter
1092 ])
1093
1094 # failed tasks
1095 criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
1096 filter_only_builds_with_failed_tasks = QuerysetFilter(criteria)
1097 with_failed_tasks_filter = self.make_filter_action(
1098 'with_failed_tasks',
1099 'Builds with failed tasks',
1100 filter_only_builds_with_failed_tasks
1101 )
1102
1103 criteria = ~Q(task_build__outcome=Task.OUTCOME_FAILED)
1104 filter_only_builds_without_failed_tasks = QuerysetFilter(criteria)
1105 without_failed_tasks_filter = self.make_filter_action(
1106 'without_failed_tasks',
1107 'Builds without failed tasks',
1108 filter_only_builds_without_failed_tasks
1109 )
1110
1111 self.add_filter(title='Filter builds by failed tasks',
1112 name='failed_tasks_filter',
1113 filter_actions = [
1114 with_failed_tasks_filter,
1115 without_failed_tasks_filter
1116 ])
diff --git a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
new file mode 100644
index 0000000000..419d2b52f4
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
@@ -0,0 +1,62 @@
1{% extends 'base.html' %}
2
3{% block title %} All builds - Toaster {% endblock %}
4
5{% block pagecontent %}
6 <div class="page-header top-air">
7 <h1 data-role="page-title"></h1>
8 </div>
9
10 <div class="row-fluid">
11 {# TODO need to pass this data to context #}
12 {#% include 'mrb_section.html' %#}
13
14 {% url 'builds' as xhr_table_url %}
15 {% include 'toastertable.html' %}
16 </div>
17
18 <script>
19 $(document).ready(function () {
20 var tableElt = $("#{{table_name}}");
21 var titleElt = $("[data-role='page-title']");
22
23 tableElt.on("table-done", function (e, total, tableParams) {
24 var title = "All builds";
25
26 if (tableParams.search || tableParams.filter) {
27 if (total === 0) {
28 title = "No builds found";
29 }
30 else if (total > 0) {
31 title = total + " build" + (total > 1 ? 's' : '') + " found";
32 }
33 }
34
35 titleElt.text(title);
36 });
37
38 /* {% if last_date_from and last_date_to %}
39 // TODO initialize the date range controls;
40 // this will need to be added via ToasterTable
41 date_init(
42 "started_on",
43 "{{last_date_from}}",
44 "{{last_date_to}}",
45 "{{dateMin_started_on}}",
46 "{{dateMax_started_on}}",
47 "{{daterange_selected}}"
48 );
49
50 date_init(
51 "completed_on",
52 "{{last_date_from}}",
53 "{{last_date_to}}",
54 "{{dateMin_completed_on}}",
55 "{{dateMax_completed_on}}",
56 "{{daterange_selected}}"
57 );
58 {% endif %}
59 */
60 });
61 </script>
62{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index b5e9a0554d..707b7d5f20 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -27,7 +27,10 @@ urlpatterns = patterns('toastergui.views',
27 # landing page 27 # landing page
28 url(r'^landing/$', 'landing', name='landing'), 28 url(r'^landing/$', 'landing', name='landing'),
29 29
30 url(r'^builds/$', 'builds', name='all-builds'), 30 url(r'^builds/$',
31 tables.BuildsTable.as_view(template_name="builds-toastertable.html"),
32 name='all-builds'),
33
31 # build info navigation 34 # build info navigation
32 url(r'^build/(?P<build_id>\d+)$', 'builddashboard', name="builddashboard"), 35 url(r'^build/(?P<build_id>\d+)$', 'builddashboard', name="builddashboard"),
33 36
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index a79261de96..295773fc66 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -1915,34 +1915,6 @@ if True:
1915 ''' The exception raised on invalid POST requests ''' 1915 ''' The exception raised on invalid POST requests '''
1916 pass 1916 pass
1917 1917
1918 # shows the "all builds" page for managed mode; it displays build requests (at least started!) instead of actual builds
1919 # WARNING _build_list_helper() may raise a RedirectException, which
1920 # will set the GET parameters and redirect back to the
1921 # all-builds or projectbuilds page as appropriate;
1922 # TODO don't use exceptions to control program flow
1923 @_template_renderer("builds.html")
1924 def builds(request):
1925 # define here what parameters the view needs in the GET portion in order to
1926 # be able to display something. 'count' and 'page' are mandatory for all views
1927 # that use paginators.
1928
1929 queryset = Build.objects.all()
1930
1931 redirect_page = resolve(request.path_info).url_name
1932
1933 context, pagesize, orderby = _build_list_helper(request,
1934 queryset,
1935 redirect_page)
1936 # all builds page as a Project column
1937 context['tablecols'].append({
1938 'name': 'Project',
1939 'clclass': 'project_column'
1940 })
1941
1942 _set_parameters_values(pagesize, orderby, request)
1943 return context
1944
1945
1946 # helper function, to be used on "all builds" and "project builds" pages 1918 # helper function, to be used on "all builds" and "project builds" pages
1947 def _build_list_helper(request, queryset_all, redirect_page, pid=None): 1919 def _build_list_helper(request, queryset_all, redirect_page, pid=None):
1948 default_orderby = 'completed_on:-' 1920 default_orderby = 'completed_on:-'
@@ -1986,10 +1958,6 @@ if True:
1986 warnings_no = Count('logmessage', only=q_warnings) 1958 warnings_no = Count('logmessage', only=q_warnings)
1987 ) 1959 )
1988 1960
1989 # add timespent field
1990 timespent = 'completed_on - started_on'
1991 queryset_all = queryset_all.extra(select={'timespent': timespent})
1992
1993 queryset_with_search = _get_queryset(Build, queryset_all, 1961 queryset_with_search = _get_queryset(Build, queryset_all,
1994 None, search_term, 1962 None, search_term,
1995 ordering_string, '-completed_on') 1963 ordering_string, '-completed_on')
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index 6bb388936c..71b29eaa1e 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -32,6 +32,7 @@ from django.template import Context, Template
32from django.core.serializers.json import DjangoJSONEncoder 32from django.core.serializers.json import DjangoJSONEncoder
33from django.core.exceptions import FieldError 33from django.core.exceptions import FieldError
34from django.conf.urls import url, patterns 34from django.conf.urls import url, patterns
35from toastergui.querysetfilter import QuerysetFilter
35 36
36import types 37import types
37import json 38import json
@@ -113,7 +114,8 @@ class ToasterTable(TemplateView):
113 cls=DjangoJSONEncoder) 114 cls=DjangoJSONEncoder)
114 else: 115 else:
115 for actions in self.filters[name]['filter_actions']: 116 for actions in self.filters[name]['filter_actions']:
116 actions['count'] = self.filter_actions[actions['name']](count_only=True) 117 queryset_filter = self.filter_actions[actions['name']]
118 actions['count'] = queryset_filter.count(self.queryset)
117 119
118 # Add the "All" items filter action 120 # Add the "All" items filter action
119 self.filters[name]['filter_actions'].insert(0, { 121 self.filters[name]['filter_actions'].insert(0, {
@@ -151,15 +153,18 @@ class ToasterTable(TemplateView):
151 'filter_actions' : filter_actions, 153 'filter_actions' : filter_actions,
152 } 154 }
153 155
154 def make_filter_action(self, name, title, action_function): 156 def make_filter_action(self, name, title, queryset_filter):
155 """ Utility to make a filter_action """ 157 """
158 Utility to make a filter_action; queryset_filter is an instance
159 of QuerysetFilter or a function
160 """
156 161
157 action = { 162 action = {
158 'title' : title, 163 'title' : title,
159 'name' : name, 164 'name' : name,
160 } 165 }
161 166
162 self.filter_actions[name] = action_function 167 self.filter_actions[name] = queryset_filter
163 168
164 return action 169 return action
165 170
@@ -222,7 +227,8 @@ class ToasterTable(TemplateView):
222 return 227 return
223 228
224 try: 229 try:
225 self.filter_actions[filter_action]() 230 queryset_filter = self.filter_actions[filter_action]
231 self.queryset = queryset_filter.filter(self.queryset)
226 except KeyError: 232 except KeyError:
227 # pass it to the user - programming error here 233 # pass it to the user - programming error here
228 raise 234 raise