blob: 2cc2f4eb7bc0f3d212c11cc880493eb5118e1ef1 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4#
5# BitBake Toaster Implementation
6#
7# Copyright (C) 2015 Intel Corporation
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22from toastergui.widgets import ToasterTable
23from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050024from orm.models import CustomImageRecipe, Package, Target, Build, LogMessage, Task
25from orm.models import CustomImagePackage
26from django.db.models import Q, Max, Sum, Count, When, Case, Value, IntegerField
Patrick Williamsc124f4f2015-09-15 14:41:29 -050027from django.conf.urls import url
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050028from django.core.urlresolvers import reverse, resolve
29from django.http import HttpResponse
Patrick Williamsc124f4f2015-09-15 14:41:29 -050030from django.views.generic import TemplateView
31
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050032from toastergui.tablefilter import TableFilter
33from toastergui.tablefilter import TableFilterActionToggle
34from toastergui.tablefilter import TableFilterActionDateRange
35from toastergui.tablefilter import TableFilterActionDay
Patrick Williamsc124f4f2015-09-15 14:41:29 -050036
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050037class ProjectFilters(object):
38 @staticmethod
39 def in_project(project_layers):
40 return Q(layer_version__in=project_layers)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050041
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050042 @staticmethod
43 def not_in_project(project_layers):
44 return ~(ProjectFilters.in_project(project_layers))
Patrick Williamsc124f4f2015-09-15 14:41:29 -050045
46class LayersTable(ToasterTable):
47 """Table of layers in Toaster"""
48
49 def __init__(self, *args, **kwargs):
50 super(LayersTable, self).__init__(*args, **kwargs)
51 self.default_orderby = "layer__name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -050052 self.title = "Compatible layers"
Patrick Williamsc124f4f2015-09-15 14:41:29 -050053
54 def get_context_data(self, **kwargs):
55 context = super(LayersTable, self).get_context_data(**kwargs)
56
57 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -050058 context['project'] = project
Patrick Williamsc124f4f2015-09-15 14:41:29 -050059
60 return context
61
Patrick Williamsc124f4f2015-09-15 14:41:29 -050062 def setup_filters(self, *args, **kwargs):
63 project = Project.objects.get(pk=kwargs['pid'])
64 self.project_layers = ProjectLayer.objects.filter(project=project)
65
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050066 in_current_project_filter = TableFilter(
67 "in_current_project",
68 "Filter by project layers"
69 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050070
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050071 criteria = Q(projectlayer__in=self.project_layers)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050072
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050073 in_project_action = TableFilterActionToggle(
74 "in_project",
75 "Layers added to this project",
76 criteria
77 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050078
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050079 not_in_project_action = TableFilterActionToggle(
80 "not_in_project",
81 "Layers not added to this project",
82 ~criteria
83 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050084
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050085 in_current_project_filter.add_action(in_project_action)
86 in_current_project_filter.add_action(not_in_project_action)
87 self.add_filter(in_current_project_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050088
89 def setup_queryset(self, *args, **kwargs):
90 prj = Project.objects.get(pk = kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -050091 compatible_layers = prj.get_all_compatible_layer_versions()
92
93 self.static_context_extra['current_layers'] = \
94 prj.get_project_layer_versions(pk=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050095
96 self.queryset = compatible_layers.order_by(self.default_orderby)
97
98 def setup_columns(self, *args, **kwargs):
99
100 layer_link_template = '''
101 <a href="{% url 'layerdetails' extra.pid data.id %}">
102 {{data.layer.name}}
103 </a>
104 '''
105
106 self.add_column(title="Layer",
107 hideable=False,
108 orderable=True,
109 static_data_name="layer__name",
110 static_data_template=layer_link_template)
111
112 self.add_column(title="Summary",
113 field_name="layer__summary")
114
115 git_url_template = '''
116 <a href="{% url 'layerdetails' extra.pid data.id %}">
117 <code>{{data.layer.vcs_url}}</code>
118 </a>
119 {% if data.get_vcs_link_url %}
120 <a target="_blank" href="{{ data.get_vcs_link_url }}">
121 <i class="icon-share get-info"></i>
122 </a>
123 {% endif %}
124 '''
125
126 self.add_column(title="Git repository URL",
127 help_text="The Git repository for the layer source code",
128 hidden=True,
129 static_data_name="layer__vcs_url",
130 static_data_template=git_url_template)
131
132 git_dir_template = '''
133 <a href="{% url 'layerdetails' extra.pid data.id %}">
134 <code>{{data.dirpath}}</code>
135 </a>
136 {% if data.dirpath and data.get_vcs_dirpath_link_url %}
137 <a target="_blank" href="{{ data.get_vcs_dirpath_link_url }}">
138 <i class="icon-share get-info"></i>
139 </a>
140 {% endif %}'''
141
142 self.add_column(title="Subdirectory",
143 help_text="The layer directory within the Git repository",
144 hidden=True,
145 static_data_name="git_subdir",
146 static_data_template=git_dir_template)
147
148 revision_template = '''
149 {% load projecttags %}
150 {% with vcs_ref=data.get_vcs_reference %}
151 {% if vcs_ref|is_shaid %}
152 <a class="btn" data-content="<ul class='unstyled'> <li>{{vcs_ref}}</li> </ul>">
153 {{vcs_ref|truncatechars:10}}
154 </a>
155 {% else %}
156 {{vcs_ref}}
157 {% endif %}
158 {% endwith %}
159 '''
160
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500161 self.add_column(title="Git revision",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500162 help_text="The Git branch, tag or commit. For the layers from the OpenEmbedded layer source, the revision is always the branch compatible with the Yocto Project version you selected for this project",
163 static_data_name="revision",
164 static_data_template=revision_template)
165
166 deps_template = '''
167 {% with ods=data.dependencies.all%}
168 {% if ods.count %}
169 <a class="btn" title="<a href='{% url "layerdetails" extra.pid data.id %}'>{{data.layer.name}}</a> dependencies"
170 data-content="<ul class='unstyled'>
171 {% for i in ods%}
172 <li><a href='{% url "layerdetails" extra.pid i.depends_on.pk %}'>{{i.depends_on.layer.name}}</a></li>
173 {% endfor %}
174 </ul>">
175 {{ods.count}}
176 </a>
177 {% endif %}
178 {% endwith %}
179 '''
180
181 self.add_column(title="Dependencies",
182 help_text="Other layers a layer depends upon",
183 static_data_name="dependencies",
184 static_data_template=deps_template)
185
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500186 self.add_column(title="Add | Remove",
187 help_text="Add or remove layers to / from your project",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500188 hideable=False,
189 filter_name="in_current_project",
190 static_data_name="add-del-layers",
191 static_data_template='{% include "layer_btn.html" %}')
192
193 project = Project.objects.get(pk=kwargs['pid'])
194 self.add_column(title="LayerDetailsUrl",
195 displayable = False,
196 field_name="layerdetailurl",
197 computation = lambda x: reverse('layerdetails', args=(project.id, x.id)))
198
199 self.add_column(title="name",
200 displayable = False,
201 field_name="name",
202 computation = lambda x: x.layer.name)
203
204
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500205class MachinesTable(ToasterTable):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500206 """Table of Machines in Toaster"""
207
208 def __init__(self, *args, **kwargs):
209 super(MachinesTable, self).__init__(*args, **kwargs)
210 self.empty_state = "No machines maybe you need to do a build?"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500211 self.title = "Compatible machines"
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500212 self.default_orderby = "name"
213
214 def get_context_data(self, **kwargs):
215 context = super(MachinesTable, self).get_context_data(**kwargs)
216 context['project'] = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500217 return context
218
219 def setup_filters(self, *args, **kwargs):
220 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500221
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500222 in_current_project_filter = TableFilter(
223 "in_current_project",
224 "Filter by project machines"
225 )
226
227 in_project_action = TableFilterActionToggle(
228 "in_project",
229 "Machines provided by layers added to this project",
230 ProjectFilters.in_project(self.project_layers)
231 )
232
233 not_in_project_action = TableFilterActionToggle(
234 "not_in_project",
235 "Machines provided by layers not added to this project",
236 ProjectFilters.not_in_project(self.project_layers)
237 )
238
239 in_current_project_filter.add_action(in_project_action)
240 in_current_project_filter.add_action(not_in_project_action)
241 self.add_filter(in_current_project_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500242
243 def setup_queryset(self, *args, **kwargs):
244 prj = Project.objects.get(pk = kwargs['pid'])
245 self.queryset = prj.get_all_compatible_machines()
246 self.queryset = self.queryset.order_by(self.default_orderby)
247
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500248 self.static_context_extra['current_layers'] = \
249 self.project_layers = \
250 prj.get_project_layer_versions(pk=True)
251
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500252 def setup_columns(self, *args, **kwargs):
253
254 self.add_column(title="Machine",
255 hideable=False,
256 orderable=True,
257 field_name="name")
258
259 self.add_column(title="Description",
260 field_name="description")
261
262 layer_link_template = '''
263 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
264 {{data.layer_version.layer.name}}</a>
265 '''
266
267 self.add_column(title="Layer",
268 static_data_name="layer_version__layer__name",
269 static_data_template=layer_link_template,
270 orderable=True)
271
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500272 self.add_column(title="Git revision",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500273 help_text="The Git branch, tag or commit. For the layers from the OpenEmbedded layer source, the revision is always the branch compatible with the Yocto Project version you selected for this project",
274 hidden=True,
275 field_name="layer_version__get_vcs_reference")
276
277 machine_file_template = '''<code>conf/machine/{{data.name}}.conf</code>
278 <a href="{{data.get_vcs_machine_file_link_url}}" target="_blank"><i class="icon-share get-info"></i></a>'''
279
280 self.add_column(title="Machine file",
281 hidden=True,
282 static_data_name="machinefile",
283 static_data_template=machine_file_template)
284
285 self.add_column(title="Select",
286 help_text="Sets the selected machine as the project machine. You can only have one machine per project",
287 hideable=False,
288 filter_name="in_current_project",
289 static_data_name="add-del-layers",
290 static_data_template='{% include "machine_btn.html" %}')
291
292
293class LayerMachinesTable(MachinesTable):
294 """ Smaller version of the Machines table for use in layer details """
295
296 def __init__(self, *args, **kwargs):
297 super(LayerMachinesTable, self).__init__(*args, **kwargs)
298
299 def get_context_data(self, **kwargs):
300 context = super(LayerMachinesTable, self).get_context_data(**kwargs)
301 context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
302 return context
303
304
305 def setup_queryset(self, *args, **kwargs):
306 MachinesTable.setup_queryset(self, *args, **kwargs)
307
308 self.queryset = self.queryset.filter(layer_version__pk=int(kwargs['layerid']))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500309 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500310 self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
311
312 def setup_columns(self, *args, **kwargs):
313 self.add_column(title="Machine",
314 hideable=False,
315 orderable=True,
316 field_name="name")
317
318 self.add_column(title="Description",
319 field_name="description")
320
321 select_btn_template = '<a href="{% url "project" extra.pid %}?setMachine={{data.name}}" class="btn btn-block select-machine-btn" {% if extra.in_prj == 0%}disabled="disabled"{%endif%}>Select machine</a>'
322
323 self.add_column(title="Select machine",
324 static_data_name="add-del-layers",
325 static_data_template=select_btn_template)
326
327
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500328class RecipesTable(ToasterTable):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500329 """Table of All Recipes in Toaster"""
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500330
331 def __init__(self, *args, **kwargs):
332 super(RecipesTable, self).__init__(*args, **kwargs)
333 self.empty_state = "Toaster has no recipe information. To generate recipe information you can configure a layer source then run a build."
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500334
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500335 build_col = { 'title' : "Build",
336 'help_text' : "Add or delete recipes to and from your project",
337 'hideable' : False,
338 'filter_name' : "in_current_project",
339 'static_data_name' : "add-del-layers",
340 'static_data_template' : '{% include "recipe_btn.html" %}'}
341
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500342 def get_context_data(self, **kwargs):
343 project = Project.objects.get(pk=kwargs['pid'])
344 context = super(RecipesTable, self).get_context_data(**kwargs)
345
346 context['project'] = project
347
348 context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project']))
349
350 return context
351
352 def setup_filters(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500353 table_filter = TableFilter(
354 'in_current_project',
355 'Filter by project recipes'
356 )
357
358 in_project_action = TableFilterActionToggle(
359 'in_project',
360 'Recipes provided by layers added to this project',
361 ProjectFilters.in_project(self.project_layers)
362 )
363
364 not_in_project_action = TableFilterActionToggle(
365 'not_in_project',
366 'Recipes provided by layers not added to this project',
367 ProjectFilters.not_in_project(self.project_layers)
368 )
369
370 table_filter.add_action(in_project_action)
371 table_filter.add_action(not_in_project_action)
372 self.add_filter(table_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500373
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500374 def setup_queryset(self, *args, **kwargs):
375 prj = Project.objects.get(pk = kwargs['pid'])
376
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500377 # Project layers used by the filters
378 self.project_layers = prj.get_project_layer_versions(pk=True)
379
380 # Project layers used to switch the button states
381 self.static_context_extra['current_layers'] = self.project_layers
382
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500383 self.queryset = prj.get_all_compatible_recipes()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500384
385
386 def setup_columns(self, *args, **kwargs):
387
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500388 self.add_column(title="Version",
389 hidden=False,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500390 field_name="version")
391
392 self.add_column(title="Description",
393 field_name="get_description_or_summary")
394
395 recipe_file_template = '''
396 <code>{{data.file_path}}</code>
397 <a href="{{data.get_vcs_recipe_file_link_url}}" target="_blank">
398 <i class="icon-share get-info"></i>
399 </a>
400 '''
401
402 self.add_column(title="Recipe file",
403 help_text="Path to the recipe .bb file",
404 hidden=True,
405 static_data_name="recipe-file",
406 static_data_template=recipe_file_template)
407
408 self.add_column(title="Section",
409 help_text="The section in which recipes should be categorized",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500410 hidden=True,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500411 orderable=True,
412 field_name="section")
413
414 layer_link_template = '''
415 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
416 {{data.layer_version.layer.name}}</a>
417 '''
418
419 self.add_column(title="Layer",
420 help_text="The name of the layer providing the recipe",
421 orderable=True,
422 static_data_name="layer_version__layer__name",
423 static_data_template=layer_link_template)
424
425 self.add_column(title="License",
426 help_text="The list of source licenses for the recipe. Multiple license names separated by the pipe character indicates a choice between licenses. Multiple license names separated by the ampersand character indicates multiple licenses exist that cover different parts of the source",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500427 hidden=True,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500428 orderable=True,
429 field_name="license")
430
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500431 self.add_column(title="Git revision",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500432 hidden=True,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500433 field_name="layer_version__get_vcs_reference")
434
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500435
436class LayerRecipesTable(RecipesTable):
437 """ Smaller version of the Recipes table for use in layer details """
438
439 def __init__(self, *args, **kwargs):
440 super(LayerRecipesTable, self).__init__(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500441 self.default_orderby = "name"
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500442
443 def get_context_data(self, **kwargs):
444 context = super(LayerRecipesTable, self).get_context_data(**kwargs)
445 context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
446 return context
447
448
449 def setup_queryset(self, *args, **kwargs):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500450 self.queryset = \
451 Recipe.objects.filter(layer_version__pk=int(kwargs['layerid']))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500452
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500453 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500454 self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
455
456 def setup_columns(self, *args, **kwargs):
457 self.add_column(title="Recipe",
458 help_text="Information about a single piece of software, including where to download the source, configuration options, how to compile the source files and how to package the compiled output",
459 hideable=False,
460 orderable=True,
461 field_name="name")
462
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500463 self.add_column(title="Version",
464 field_name="version")
465
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500466 self.add_column(title="Description",
467 field_name="get_description_or_summary")
468
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500469 build_recipe_template ='<button class="btn btn-block build-recipe-btn" data-recipe-name="{{data.name}}" {%if extra.in_prj == 0 %}disabled="disabled"{%endif%}>Build recipe</button>'
470
471 self.add_column(title="Build recipe",
472 static_data_name="add-del-layers",
473 static_data_template=build_recipe_template)
474
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500475class CustomImagesTable(ToasterTable):
476 """ Table to display your custom images """
477 def __init__(self, *args, **kwargs):
478 super(CustomImagesTable, self).__init__(*args, **kwargs)
479 self.title = "Custom images"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500480 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500481
482 def get_context_data(self, **kwargs):
483 context = super(CustomImagesTable, self).get_context_data(**kwargs)
484 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500485 # TODO put project into the ToasterTable base class
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500486 context['project'] = project
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500487 return context
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500488
489 def setup_queryset(self, *args, **kwargs):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500490 prj = Project.objects.get(pk = kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500491 self.queryset = CustomImageRecipe.objects.filter(project=prj)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500492 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500493
494 def setup_columns(self, *args, **kwargs):
495
496 name_link_template = '''
497 <a href="{% url 'customrecipe' extra.pid data.id %}">
498 {{data.name}}
499 </a>
500 '''
501
502 self.add_column(title="Custom image",
503 hideable=False,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500504 orderable=True,
505 field_name="name",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500506 static_data_name="name",
507 static_data_template=name_link_template)
508
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500509 recipe_file_template = '''
510 <code>{{data.name}}_{{data.version}}.bb</code>
511 <a href="{% url 'customrecipedownload' extra.pid data.pk %}">
512 <i class="icon-download-alt" data-original-title="Download recipe
513 file"></i>
514 </a>'''
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500515
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500516 self.add_column(title="Recipe file",
517 static_data_name='recipe_file_download',
518 static_data_template=recipe_file_template)
519
520 approx_packages_template = '''
521 <a href="{% url 'customrecipe' extra.pid data.id %}">
522 {{data.get_all_packages.count}}
523 </a>'''
524
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500525 self.add_column(title="Approx packages",
526 static_data_name='approx_packages',
527 static_data_template=approx_packages_template)
528
529
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500530 build_btn_template = '''
531 <button data-recipe-name="{{data.name}}"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500532 class="btn btn-block build-recipe-btn" style="margin-top: 5px;" >
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500533 Build
534 </button>'''
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500535
536 self.add_column(title="Build",
537 hideable=False,
538 static_data_name='build_custom_img',
539 static_data_template=build_btn_template)
540
541class ImageRecipesTable(RecipesTable):
542 """ A subset of the recipes table which displayed just image recipes """
543
544 def __init__(self, *args, **kwargs):
545 super(ImageRecipesTable, self).__init__(*args, **kwargs)
546 self.title = "Compatible image recipes"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500547 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500548
549 def setup_queryset(self, *args, **kwargs):
550 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
551
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500552 custom_image_recipes = CustomImageRecipe.objects.filter(
553 project=kwargs['pid'])
554 self.queryset = self.queryset.filter(
555 Q(is_image=True) & ~Q(pk__in=custom_image_recipes))
556 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500557
558
559 def setup_columns(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500560
561 name_link_template = '''
562 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
563 '''
564
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500565 self.add_column(title="Image recipe",
566 help_text="When you build an image recipe, you get an "
567 "image: a root file system you can"
568 "deploy to a machine",
569 hideable=False,
570 orderable=True,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500571 static_data_name="name",
572 static_data_template=name_link_template,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500573 field_name="name")
574
575 super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
576
577 self.add_column(**RecipesTable.build_col)
578
579
580class NewCustomImagesTable(ImageRecipesTable):
581 """ Table which displays Images recipes which can be customised """
582 def __init__(self, *args, **kwargs):
583 super(NewCustomImagesTable, self).__init__(*args, **kwargs)
584 self.title = "Select the image recipe you want to customise"
585
586 def setup_queryset(self, *args, **kwargs):
587 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500588 prj = Project.objects.get(pk = kwargs['pid'])
589 self.static_context_extra['current_layers'] = \
590 prj.get_project_layer_versions(pk=True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500591
592 self.queryset = self.queryset.filter(is_image=True)
593
594 def setup_columns(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500595
596 name_link_template = '''
597 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
598 '''
599
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500600 self.add_column(title="Image recipe",
601 help_text="When you build an image recipe, you get an "
602 "image: a root file system you can"
603 "deploy to a machine",
604 hideable=False,
605 orderable=True,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500606 static_data_name="name",
607 static_data_template=name_link_template,
608 field_name="name")
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500609
610 super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
611
612 self.add_column(title="Customise",
613 hideable=False,
614 filter_name="in_current_project",
615 static_data_name="customise-or-add-recipe",
616 static_data_template='{% include "customise_btn.html" %}')
617
618
619class SoftwareRecipesTable(RecipesTable):
620 """ Displays just the software recipes """
621 def __init__(self, *args, **kwargs):
622 super(SoftwareRecipesTable, self).__init__(*args, **kwargs)
623 self.title = "Compatible software recipes"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500624 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500625
626 def setup_queryset(self, *args, **kwargs):
627 super(SoftwareRecipesTable, self).setup_queryset(*args, **kwargs)
628
629 self.queryset = self.queryset.filter(is_image=False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500630 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500631
632
633 def setup_columns(self, *args, **kwargs):
634 self.add_column(title="Software recipe",
635 help_text="Information about a single piece of "
636 "software, including where to download the source, "
637 "configuration options, how to compile the source "
638 "files and how to package the compiled output",
639 hideable=False,
640 orderable=True,
641 field_name="name")
642
643 super(SoftwareRecipesTable, self).setup_columns(*args, **kwargs)
644
645 self.add_column(**RecipesTable.build_col)
646
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500647class PackagesTable(ToasterTable):
648 """ Table to display the packages in a recipe from it's last successful
649 build"""
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500650
651 def __init__(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500652 super(PackagesTable, self).__init__(*args, **kwargs)
653 self.title = "Packages included"
654 self.packages = None
655 self.default_orderby = "name"
656
657 def create_package_list(self, recipe, project_id):
658 """Creates a list of packages for the specified recipe by looking for
659 the last SUCCEEDED build of ther recipe"""
660
661 target = Target.objects.filter(Q(target=recipe.name) &
662 Q(build__project_id=project_id) &
663 Q(build__outcome=Build.SUCCEEDED)
664 ).last()
665
666 if target:
667 pkgs = target.target_installed_package_set.values_list('package',
668 flat=True)
669 return Package.objects.filter(pk__in=pkgs)
670
671 # Target/recipe never successfully built so empty queryset
672 return Package.objects.none()
673
674 def get_context_data(self, **kwargs):
675 """Context for rendering the sidebar and other items on the recipe
676 details page """
677 context = super(PackagesTable, self).get_context_data(**kwargs)
678
679 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
680 project = Project.objects.get(pk=kwargs['pid'])
681
682 in_project = (recipe.layer_version.pk in
683 project.get_project_layer_versions(pk=True))
684
685 packages = self.create_package_list(recipe, project.pk)
686
687 context.update({'project': project,
688 'recipe' : recipe,
689 'packages': packages,
690 'approx_pkg_size' : packages.aggregate(Sum('size')),
691 'in_project' : in_project,
692 })
693
694 return context
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500695
696 def setup_queryset(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500697 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500698
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500699 self.queryset = self.create_package_list(recipe, kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500700 self.queryset = self.queryset.order_by('name')
701
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500702 def setup_columns(self, *args, **kwargs):
703 self.add_column(title="Package",
704 hideable=False,
705 orderable=True,
706 field_name="name")
707
708 self.add_column(title="Package Version",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500709 field_name="version",
710 hideable=False)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500711
712 self.add_column(title="Approx Size",
713 orderable=True,
714 static_data_name="size",
715 static_data_template="{% load projecttags %} \
716 {{data.size|filtered_filesizeformat}}")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500717
718 self.add_column(title="License",
719 field_name="license",
720 orderable=True)
721
722
723 self.add_column(title="Dependencies",
724 static_data_name="dependencies",
725 static_data_template='\
726 {% include "snippets/pkg_dependencies_popover.html" %}')
727
728 self.add_column(title="Reverse dependencies",
729 static_data_name="reverse_dependencies",
730 static_data_template='\
731 {% include "snippets/pkg_revdependencies_popover.html" %}',
732 hidden=True)
733
734 self.add_column(title="Recipe",
735 field_name="recipe__name",
736 orderable=True,
737 hidden=True)
738
739 self.add_column(title="Recipe version",
740 field_name="recipe__version",
741 hidden=True)
742
743
744class SelectPackagesTable(PackagesTable):
745 """ Table to display the packages to add and remove from an image """
746
747 def __init__(self, *args, **kwargs):
748 super(SelectPackagesTable, self).__init__(*args, **kwargs)
749 self.title = "Add | Remove packages"
750
751 def setup_queryset(self, *args, **kwargs):
752 self.cust_recipe =\
753 CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
754 prj = Project.objects.get(pk = kwargs['pid'])
755
756 current_packages = self.cust_recipe.get_all_packages()
757
758 current_recipes = prj.get_available_recipes()
759
760 # only show packages where recipes->layers are in the project
761 self.queryset = CustomImagePackage.objects.filter(
762 ~Q(recipe=None) &
763 Q(recipe__in=current_recipes))
764
765 self.queryset = self.queryset.order_by('name')
766
767 self.static_context_extra['recipe_id'] = kwargs['custrecipeid']
768 self.static_context_extra['current_packages'] = \
769 current_packages.values_list('pk', flat=True)
770
771 def get_context_data(self, **kwargs):
772 # to reuse the Super class map the custrecipeid to the recipe_id
773 kwargs['recipe_id'] = kwargs['custrecipeid']
774 context = super(SelectPackagesTable, self).get_context_data(**kwargs)
775 custom_recipe = \
776 CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
777
778 context['recipe'] = custom_recipe
779 context['approx_pkg_size'] = \
780 custom_recipe.get_all_packages().aggregate(Sum('size'))
781 return context
782
783
784 def setup_columns(self, *args, **kwargs):
785 super(SelectPackagesTable, self).setup_columns(*args, **kwargs)
786
787 add_remove_template = '{% include "pkg_add_rm_btn.html" %}'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500788
789 self.add_column(title="Add | Remove",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500790 hideable=False,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500791 help_text="Use the add and remove buttons to modify "
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500792 "the package content of your custom image",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500793 static_data_name="add_rm_pkg_btn",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500794 static_data_template=add_remove_template,
795 filter_name='in_current_image_filter')
796
797 def setup_filters(self, *args, **kwargs):
798 in_current_image_filter = TableFilter(
799 'in_current_image_filter',
800 'Filter by added packages'
801 )
802
803 in_image_action = TableFilterActionToggle(
804 'in_image',
805 'Packages in %s' % self.cust_recipe.name,
806 Q(pk__in=self.static_context_extra['current_packages'])
807 )
808
809 not_in_image_action = TableFilterActionToggle(
810 'not_in_image',
811 'Packages not added to %s' % self.cust_recipe.name,
812 ~Q(pk__in=self.static_context_extra['current_packages'])
813 )
814
815 in_current_image_filter.add_action(in_image_action)
816 in_current_image_filter.add_action(not_in_image_action)
817 self.add_filter(in_current_image_filter)
818
819class ProjectsTable(ToasterTable):
820 """Table of projects in Toaster"""
821
822 def __init__(self, *args, **kwargs):
823 super(ProjectsTable, self).__init__(*args, **kwargs)
824 self.default_orderby = '-updated'
825 self.title = 'All projects'
826 self.static_context_extra['Build'] = Build
827
828 def get_context_data(self, **kwargs):
829 return super(ProjectsTable, self).get_context_data(**kwargs)
830
831 def setup_queryset(self, *args, **kwargs):
832 queryset = Project.objects.all()
833
834 # annotate each project with its number of builds
835 queryset = queryset.annotate(num_builds=Count('build'))
836
837 # exclude the command line builds project if it has no builds
838 q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0)
839 queryset = queryset.filter(Q(is_default=False) |
840 q_default_with_builds)
841
842 # order rows
843 queryset = queryset.order_by(self.default_orderby)
844
845 self.queryset = queryset
846
847 # columns: last activity on (updated) - DEFAULT, project (name), release,
848 # machine, number of builds, last build outcome, recipe (name), errors,
849 # warnings, image files
850 def setup_columns(self, *args, **kwargs):
851 name_template = '''
852 {% load project_url_tag %}
853 <span data-project-field="name">
854 <a href="{% project_url data %}">
855 {{data.name}}
856 </a>
857 </span>
858 '''
859
860 last_activity_on_template = '''
861 {% load project_url_tag %}
862 <span data-project-field="updated">
863 <a href="{% project_url data %}">
864 {{data.updated | date:"d/m/y H:i"}}
865 </a>
866 </span>
867 '''
868
869 release_template = '''
870 <span data-project-field="release">
871 {% if data.release %}
872 <a href="{% url 'project' data.id %}#project-details">
873 {{data.release.name}}
874 </a>
875 {% elif data.is_default %}
876 <span class="muted">Not applicable</span>
877 <i class="icon-question-sign get-help hover-help"
878 data-original-title="This project does not have a release set.
879 It simply collects information about the builds you start from
880 the command line while Toaster is running"
881 style="visibility: hidden;">
882 </i>
883 {% else %}
884 No release available
885 {% endif %}
886 </span>
887 '''
888
889 machine_template = '''
890 <span data-project-field="machine">
891 {% if data.is_default %}
892 <span class="muted">Not applicable</span>
893 <i class="icon-question-sign get-help hover-help"
894 data-original-title="This project does not have a machine
895 set. It simply collects information about the builds you
896 start from the command line while Toaster is running"
897 style="visibility: hidden;"></i>
898 {% else %}
899 <a href="{% url 'project' data.id %}#machine-distro">
900 {{data.get_current_machine_name}}
901 </a>
902 {% endif %}
903 </span>
904 '''
905
906 number_of_builds_template = '''
907 {% if data.get_number_of_builds > 0 %}
908 <a href="{% url 'projectbuilds' data.id %}">
909 {{data.get_number_of_builds}}
910 </a>
911 {% else %}
912 <span class="muted">0</span>
913 {% endif %}
914 '''
915
916 last_build_outcome_template = '''
917 {% if data.get_number_of_builds > 0 %}
918 <a href="{% url 'builddashboard' data.get_last_build_id %}">
919 {% if data.get_last_outcome == extra.Build.SUCCEEDED %}
920 <i class="icon-ok-sign success"></i>
921 {% elif data.get_last_outcome == extra.Build.FAILED %}
922 <i class="icon-minus-sign error"></i>
923 {% endif %}
924 </a>
925 {% endif %}
926 '''
927
928 recipe_template = '''
929 {% if data.get_number_of_builds > 0 %}
930 <a href="{% url "builddashboard" data.get_last_build_id %}">
931 {{data.get_last_target}}
932 </a>
933 {% endif %}
934 '''
935
936 errors_template = '''
937 {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %}
938 <a class="errors.count error"
939 href="{% url "builddashboard" data.get_last_build_id %}#errors">
940 {{data.get_last_errors}} error{{data.get_last_errors | pluralize}}
941 </a>
942 {% endif %}
943 '''
944
945 warnings_template = '''
946 {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %}
947 <a class="warnings.count warning"
948 href="{% url "builddashboard" data.get_last_build_id %}#warnings">
949 {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}}
950 </a>
951 {% endif %}
952 '''
953
954 image_files_template = '''
955 {% if data.get_number_of_builds > 0 and data.get_last_outcome == extra.Build.SUCCEEDED %}
956 <a href="{% url "builddashboard" data.get_last_build_id %}#images">
957 {{data.get_last_build_extensions}}
958 </a>
959 {% endif %}
960 '''
961
962 self.add_column(title='Project',
963 hideable=False,
964 orderable=True,
965 static_data_name='name',
966 static_data_template=name_template)
967
968 self.add_column(title='Last activity on',
969 help_text='Starting date and time of the \
970 last project build. If the project has no \
971 builds, this shows the date the project was \
972 created.',
973 hideable=False,
974 orderable=True,
975 static_data_name='updated',
976 static_data_template=last_activity_on_template)
977
978 self.add_column(title='Release',
979 help_text='The version of the build system used by \
980 the project',
981 hideable=False,
982 orderable=True,
983 static_data_name='release',
984 static_data_template=release_template)
985
986 self.add_column(title='Machine',
987 help_text='The hardware currently selected for the \
988 project',
989 hideable=False,
990 orderable=False,
991 static_data_name='machine',
992 static_data_template=machine_template)
993
994 self.add_column(title='Number of builds',
995 help_text='The number of builds which have been run \
996 for the project',
997 hideable=False,
998 orderable=False,
999 static_data_name='number_of_builds',
1000 static_data_template=number_of_builds_template)
1001
1002 self.add_column(title='Last build outcome',
1003 help_text='Indicates whether the last project build \
1004 completed successfully or failed',
1005 hideable=True,
1006 orderable=False,
1007 static_data_name='last_build_outcome',
1008 static_data_template=last_build_outcome_template)
1009
1010 self.add_column(title='Recipe',
1011 help_text='The last recipe which was built in this \
1012 project',
1013 hideable=True,
1014 orderable=False,
1015 static_data_name='recipe_name',
1016 static_data_template=recipe_template)
1017
1018 self.add_column(title='Errors',
1019 help_text='The number of errors encountered during \
1020 the last project build (if any)',
1021 hideable=True,
1022 orderable=False,
1023 static_data_name='errors',
1024 static_data_template=errors_template)
1025
1026 self.add_column(title='Warnings',
1027 help_text='The number of warnings encountered during \
1028 the last project build (if any)',
1029 hideable=True,
1030 hidden=True,
1031 orderable=False,
1032 static_data_name='warnings',
1033 static_data_template=warnings_template)
1034
1035 self.add_column(title='Image files',
1036 help_text='The root file system types produced by \
1037 the last project build',
1038 hideable=True,
1039 hidden=True,
1040 orderable=False,
1041 static_data_name='image_files',
1042 static_data_template=image_files_template)
1043
1044class BuildsTable(ToasterTable):
1045 """Table of builds in Toaster"""
1046
1047 def __init__(self, *args, **kwargs):
1048 super(BuildsTable, self).__init__(*args, **kwargs)
1049 self.default_orderby = '-completed_on'
1050 self.static_context_extra['Build'] = Build
1051 self.static_context_extra['Task'] = Task
1052
1053 # attributes that are overridden in subclasses
1054
1055 # title for the page
1056 self.title = ''
1057
1058 # 'project' or 'all'; determines how the mrb (most recent builds)
1059 # section is displayed
1060 self.mrb_type = ''
1061
1062 def get_builds(self):
1063 """
1064 overridden in ProjectBuildsTable to return builds for a
1065 single project
1066 """
1067 return Build.objects.all()
1068
1069 def get_context_data(self, **kwargs):
1070 context = super(BuildsTable, self).get_context_data(**kwargs)
1071
1072 # should be set in subclasses
1073 context['mru'] = []
1074
1075 context['mrb_type'] = self.mrb_type
1076
1077 return context
1078
1079 def setup_queryset(self, *args, **kwargs):
1080 """
1081 The queryset is annotated so that it can be sorted by number of
1082 errors and number of warnings; but note that the criteria for
1083 finding the log messages to populate these fields should match those
1084 used in the Build model (orm/models.py) to populate the errors and
1085 warnings properties
1086 """
1087 queryset = self.get_builds()
1088
1089 # Don't include in progress builds pr cancelled builds
1090 queryset = queryset.exclude(Q(outcome=Build.IN_PROGRESS) |
1091 Q(outcome=Build.CANCELLED))
1092
1093 # sort
1094 queryset = queryset.order_by(self.default_orderby)
1095
1096 # annotate with number of ERROR, EXCEPTION and CRITICAL log messages
1097 criteria = (Q(logmessage__level=LogMessage.ERROR) |
1098 Q(logmessage__level=LogMessage.EXCEPTION) |
1099 Q(logmessage__level=LogMessage.CRITICAL))
1100
1101 queryset = queryset.annotate(
1102 errors_no=Count(
1103 Case(
1104 When(criteria, then=Value(1)),
1105 output_field=IntegerField()
1106 )
1107 )
1108 )
1109
1110 # annotate with number of WARNING log messages
1111 queryset = queryset.annotate(
1112 warnings_no=Count(
1113 Case(
1114 When(logmessage__level=LogMessage.WARNING, then=Value(1)),
1115 output_field=IntegerField()
1116 )
1117 )
1118 )
1119
1120 self.queryset = queryset
1121
1122 def setup_columns(self, *args, **kwargs):
1123 outcome_template = '''
1124 <a href="{% url "builddashboard" data.id %}">
1125 {% if data.outcome == data.SUCCEEDED %}
1126 <i class="icon-ok-sign success"></i>
1127 {% elif data.outcome == data.FAILED %}
1128 <i class="icon-minus-sign error"></i>
1129 {% endif %}
1130 </a>
1131
1132 {% if data.cooker_log_path %}
1133 &nbsp;
1134 <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
1135 <i class="icon-download-alt get-help"
1136 data-original-title="Download build log"></i>
1137 </a>
1138 {% endif %}
1139 '''
1140
1141 recipe_template = '''
1142 {% for target_label in data.target_labels %}
1143 <a href="{% url "builddashboard" data.id %}">
1144 {{target_label}}
1145 </a>
1146 <br />
1147 {% endfor %}
1148 '''
1149
1150 machine_template = '''
1151 <a href="{% url "builddashboard" data.id %}">
1152 {{data.machine}}
1153 </a>
1154 '''
1155
1156 started_on_template = '''
1157 <a href="{% url "builddashboard" data.id %}">
1158 {{data.started_on | date:"d/m/y H:i"}}
1159 </a>
1160 '''
1161
1162 completed_on_template = '''
1163 <a href="{% url "builddashboard" data.id %}">
1164 {{data.completed_on | date:"d/m/y H:i"}}
1165 </a>
1166 '''
1167
1168 failed_tasks_template = '''
1169 {% if data.failed_tasks.count == 1 %}
1170 <a href="{% url "task" data.id data.failed_tasks.0.id %}">
1171 <span class="error">
1172 {{data.failed_tasks.0.recipe.name}}.{{data.failed_tasks.0.task_name}}
1173 </span>
1174 </a>
1175 <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}">
1176 <i class="icon-download-alt"
1177 data-original-title="Download task log file">
1178 </i>
1179 </a>
1180 {% elif data.failed_tasks.count > 1 %}
1181 <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}">
1182 <span class="error">{{data.failed_tasks.count}} tasks</span>
1183 </a>
1184 {% endif %}
1185 '''
1186
1187 errors_template = '''
1188 {% if data.errors_no %}
1189 <a class="errors.count error" href="{% url "builddashboard" data.id %}#errors">
1190 {{data.errors_no}} error{{data.errors_no|pluralize}}
1191 </a>
1192 {% endif %}
1193 '''
1194
1195 warnings_template = '''
1196 {% if data.warnings_no %}
1197 <a class="warnings.count warning" href="{% url "builddashboard" data.id %}#warnings">
1198 {{data.warnings_no}} warning{{data.warnings_no|pluralize}}
1199 </a>
1200 {% endif %}
1201 '''
1202
1203 time_template = '''
1204 {% load projecttags %}
1205 <a href="{% url "buildtime" data.id %}">
1206 {{data.timespent_seconds | sectohms}}
1207 </a>
1208 '''
1209
1210 image_files_template = '''
1211 {% if data.outcome == extra.Build.SUCCEEDED %}
1212 <a href="{% url "builddashboard" data.id %}#images">
1213 {{data.get_image_file_extensions}}
1214 </a>
1215 {% endif %}
1216 '''
1217
1218 self.add_column(title='Outcome',
1219 help_text='Final state of the build (successful \
1220 or failed)',
1221 hideable=False,
1222 orderable=True,
1223 filter_name='outcome_filter',
1224 static_data_name='outcome',
1225 static_data_template=outcome_template)
1226
1227 self.add_column(title='Recipe',
1228 help_text='What was built (i.e. one or more recipes \
1229 or image recipes)',
1230 hideable=False,
1231 orderable=False,
1232 static_data_name='target',
1233 static_data_template=recipe_template)
1234
1235 self.add_column(title='Machine',
1236 help_text='Hardware for which you are building a \
1237 recipe or image recipe',
1238 hideable=False,
1239 orderable=True,
1240 static_data_name='machine',
1241 static_data_template=machine_template)
1242
1243 self.add_column(title='Started on',
1244 help_text='The date and time when the build started',
1245 hideable=True,
1246 hidden=True,
1247 orderable=True,
1248 filter_name='started_on_filter',
1249 static_data_name='started_on',
1250 static_data_template=started_on_template)
1251
1252 self.add_column(title='Completed on',
1253 help_text='The date and time when the build finished',
1254 hideable=False,
1255 orderable=True,
1256 filter_name='completed_on_filter',
1257 static_data_name='completed_on',
1258 static_data_template=completed_on_template)
1259
1260 self.add_column(title='Failed tasks',
1261 help_text='The number of tasks which failed during \
1262 the build',
1263 hideable=True,
1264 orderable=False,
1265 filter_name='failed_tasks_filter',
1266 static_data_name='failed_tasks',
1267 static_data_template=failed_tasks_template)
1268
1269 self.add_column(title='Errors',
1270 help_text='The number of errors encountered during \
1271 the build (if any)',
1272 hideable=True,
1273 orderable=True,
1274 static_data_name='errors_no',
1275 static_data_template=errors_template)
1276
1277 self.add_column(title='Warnings',
1278 help_text='The number of warnings encountered during \
1279 the build (if any)',
1280 hideable=True,
1281 orderable=True,
1282 static_data_name='warnings_no',
1283 static_data_template=warnings_template)
1284
1285 self.add_column(title='Time',
1286 help_text='How long the build took to finish',
1287 hideable=True,
1288 hidden=True,
1289 orderable=False,
1290 static_data_name='time',
1291 static_data_template=time_template)
1292
1293 self.add_column(title='Image files',
1294 help_text='The root file system types produced by \
1295 the build',
1296 hideable=True,
1297 orderable=False,
1298 static_data_name='image_files',
1299 static_data_template=image_files_template)
1300
1301 def setup_filters(self, *args, **kwargs):
1302 # outcomes
1303 outcome_filter = TableFilter(
1304 'outcome_filter',
1305 'Filter builds by outcome'
1306 )
1307
1308 successful_builds_action = TableFilterActionToggle(
1309 'successful_builds',
1310 'Successful builds',
1311 Q(outcome=Build.SUCCEEDED)
1312 )
1313
1314 failed_builds_action = TableFilterActionToggle(
1315 'failed_builds',
1316 'Failed builds',
1317 Q(outcome=Build.FAILED)
1318 )
1319
1320 outcome_filter.add_action(successful_builds_action)
1321 outcome_filter.add_action(failed_builds_action)
1322 self.add_filter(outcome_filter)
1323
1324 # started on
1325 started_on_filter = TableFilter(
1326 'started_on_filter',
1327 'Filter by date when build was started'
1328 )
1329
1330 started_today_action = TableFilterActionDay(
1331 'today',
1332 'Today\'s builds',
1333 'started_on',
1334 'today'
1335 )
1336
1337 started_yesterday_action = TableFilterActionDay(
1338 'yesterday',
1339 'Yesterday\'s builds',
1340 'started_on',
1341 'yesterday'
1342 )
1343
1344 by_started_date_range_action = TableFilterActionDateRange(
1345 'date_range',
1346 'Build date range',
1347 'started_on'
1348 )
1349
1350 started_on_filter.add_action(started_today_action)
1351 started_on_filter.add_action(started_yesterday_action)
1352 started_on_filter.add_action(by_started_date_range_action)
1353 self.add_filter(started_on_filter)
1354
1355 # completed on
1356 completed_on_filter = TableFilter(
1357 'completed_on_filter',
1358 'Filter by date when build was completed'
1359 )
1360
1361 completed_today_action = TableFilterActionDay(
1362 'today',
1363 'Today\'s builds',
1364 'completed_on',
1365 'today'
1366 )
1367
1368 completed_yesterday_action = TableFilterActionDay(
1369 'yesterday',
1370 'Yesterday\'s builds',
1371 'completed_on',
1372 'yesterday'
1373 )
1374
1375 by_completed_date_range_action = TableFilterActionDateRange(
1376 'date_range',
1377 'Build date range',
1378 'completed_on'
1379 )
1380
1381 completed_on_filter.add_action(completed_today_action)
1382 completed_on_filter.add_action(completed_yesterday_action)
1383 completed_on_filter.add_action(by_completed_date_range_action)
1384 self.add_filter(completed_on_filter)
1385
1386 # failed tasks
1387 failed_tasks_filter = TableFilter(
1388 'failed_tasks_filter',
1389 'Filter builds by failed tasks'
1390 )
1391
1392 criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
1393
1394 with_failed_tasks_action = TableFilterActionToggle(
1395 'with_failed_tasks',
1396 'Builds with failed tasks',
1397 criteria
1398 )
1399
1400 without_failed_tasks_action = TableFilterActionToggle(
1401 'without_failed_tasks',
1402 'Builds without failed tasks',
1403 ~criteria
1404 )
1405
1406 failed_tasks_filter.add_action(with_failed_tasks_action)
1407 failed_tasks_filter.add_action(without_failed_tasks_action)
1408 self.add_filter(failed_tasks_filter)
1409
1410
1411class AllBuildsTable(BuildsTable):
1412 """ Builds page for all builds """
1413
1414 def __init__(self, *args, **kwargs):
1415 super(AllBuildsTable, self).__init__(*args, **kwargs)
1416 self.title = 'All builds'
1417 self.mrb_type = 'all'
1418
1419 def setup_columns(self, *args, **kwargs):
1420 """
1421 All builds page shows a column for the project
1422 """
1423
1424 super(AllBuildsTable, self).setup_columns(*args, **kwargs)
1425
1426 project_template = '''
1427 {% load project_url_tag %}
1428 <a href="{% project_url data.project %}">
1429 {{data.project.name}}
1430 </a>
1431 {% if data.project.is_default %}
1432 <i class="icon-question-sign get-help hover-help" title=""
1433 data-original-title="This project shows information about
1434 the builds you start from the command line while Toaster is
1435 running" style="visibility: hidden;"></i>
1436 {% endif %}
1437 '''
1438
1439 self.add_column(title='Project',
1440 hideable=True,
1441 orderable=True,
1442 static_data_name='project',
1443 static_data_template=project_template)
1444
1445 def get_context_data(self, **kwargs):
1446 """ Get all builds for the recent builds area """
1447 context = super(AllBuildsTable, self).get_context_data(**kwargs)
1448 context['mru'] = Build.get_recent()
1449 return context
1450
1451class ProjectBuildsTable(BuildsTable):
1452 """
1453 Builds page for a single project; a BuildsTable, with the queryset
1454 filtered by project
1455 """
1456
1457 def __init__(self, *args, **kwargs):
1458 super(ProjectBuildsTable, self).__init__(*args, **kwargs)
1459 self.title = 'All project builds'
1460 self.mrb_type = 'project'
1461
1462 # set from the querystring
1463 self.project_id = None
1464
1465 def setup_columns(self, *args, **kwargs):
1466 """
1467 Project builds table doesn't show the machines column by default
1468 """
1469
1470 super(ProjectBuildsTable, self).setup_columns(*args, **kwargs)
1471
1472 # hide the machine column
1473 self.set_column_hidden('Machine', True)
1474
1475 # allow the machine column to be hidden by the user
1476 self.set_column_hideable('Machine', True)
1477
1478 def setup_queryset(self, *args, **kwargs):
1479 """
1480 NOTE: self.project_id must be set before calling super(),
1481 as it's used in setup_queryset()
1482 """
1483 self.project_id = kwargs['pid']
1484 super(ProjectBuildsTable, self).setup_queryset(*args, **kwargs)
1485
1486 project = Project.objects.get(pk=self.project_id)
1487 self.queryset = self.queryset.filter(project=project)
1488
1489 def get_context_data(self, **kwargs):
1490 """
1491 Get recent builds for this project, and the project itself
1492
1493 NOTE: self.project_id must be set before calling super(),
1494 as it's used in get_context_data()
1495 """
1496 self.project_id = kwargs['pid']
1497 context = super(ProjectBuildsTable, self).get_context_data(**kwargs)
1498
1499 project = Project.objects.get(pk=self.project_id)
1500 context['mru'] = Build.get_recent(project)
1501 context['project'] = project
1502
1503 return context