diff options
author | Elliot Smith <elliot.smith@intel.com> | 2016-01-15 13:00:49 +0200 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2016-01-15 16:29:59 +0000 |
commit | 294579b531d5a96a17aa863554e71f4680d35812 (patch) | |
tree | d42f10f98cb26d233860eec59cbdf0799d53ae28 /bitbake | |
parent | 6c12ca7f932d2fa06c2f2e2e6c98c76bb4d487d3 (diff) | |
download | poky-294579b531d5a96a17aa863554e71f4680d35812.tar.gz |
bitbake: toastergui: convert all builds page to ToasterTable
For better long-term maintainability, use ToasterTable instead
of Django template and view code to display the all builds page.
NB the builds.html template has been left in, as this will
otherwise cause conflicts when merging the new theme.
[YOCTO #8738]
(Bitbake rev: e0590fc8103afeb4c5e613a826057555c8193d59)
Signed-off-by: Elliot Smith <elliot.smith@intel.com>
Signed-off-by: Ed Bartosh <ed.bartosh@linux.intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake')
-rw-r--r-- | bitbake/lib/toaster/orm/models.py | 32 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/querysetfilter.py | 24 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/tables.py | 343 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/templates/builds-toastertable.html | 62 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/urls.py | 5 | ||||
-rwxr-xr-x | bitbake/lib/toaster/toastergui/views.py | 32 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/widgets.py | 16 |
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 @@ | |||
1 | class 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 | ||
22 | from toastergui.widgets import ToasterTable | 22 | from toastergui.widgets import ToasterTable |
23 | from toastergui.querysetfilter import QuerysetFilter | ||
23 | from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project | 24 | from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project |
24 | from orm.models import CustomImageRecipe, Package, Build | 25 | from orm.models import CustomImageRecipe, Package, Build, LogMessage, Task |
25 | from django.db.models import Q, Max, Count | 26 | from django.db.models import Q, Max, Count |
26 | from django.conf.urls import url | 27 | from django.conf.urls import url |
27 | from django.core.urlresolvers import reverse | 28 | from django.core.urlresolvers import reverse |
28 | from django.views.generic import TemplateView | 29 | from django.views.generic import TemplateView |
29 | 30 | ||
30 | class ProjectFiltersMixin(object): | 31 | class 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 | ||
47 | class LayersTable(ToasterTable): | 36 | class 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 | ||
207 | class MachinesTable(ToasterTable, ProjectFiltersMixin): | 183 | class 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 | ||
316 | class RecipesTable(ToasterTable, ProjectFiltersMixin): | 294 | class 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 | |||
837 | class 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 | | ||
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 | |||
32 | from django.core.serializers.json import DjangoJSONEncoder | 32 | from django.core.serializers.json import DjangoJSONEncoder |
33 | from django.core.exceptions import FieldError | 33 | from django.core.exceptions import FieldError |
34 | from django.conf.urls import url, patterns | 34 | from django.conf.urls import url, patterns |
35 | from toastergui.querysetfilter import QuerysetFilter | ||
35 | 36 | ||
36 | import types | 37 | import types |
37 | import json | 38 | import 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 |