blob: e2d23c1e81f22d929bb21d18078b857cfd04219f [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
Patrick Williamsc0f7c042017-02-23 20:41:17 -060025from orm.models import CustomImagePackage, Package_DependencyManager
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050026from 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 %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600117 {% if data.layer.local_source_dir %}
118 <code>{{data.layer.local_source_dir}}</code>
119 {% else %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500120 <code>{{data.layer.vcs_url}}</code>
121 </a>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600122 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500123 {% if data.get_vcs_link_url %}
124 <a target="_blank" href="{{ data.get_vcs_link_url }}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600125 <span class="glyphicon glyphicon-new-window"></span>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500126 </a>
127 {% endif %}
128 '''
129
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600130 self.add_column(title="Layer source code location",
131 help_text="A Git repository or an absolute path to a directory",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500132 hidden=True,
133 static_data_name="layer__vcs_url",
134 static_data_template=git_url_template)
135
136 git_dir_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600137 {% if data.layer.local_source_dir %}
138 <span class="text-muted">Not applicable</span>
139 <span class="glyphicon glyphicon-question-sign get-help" data-original-title="" title="The source code of {{data.layer.name}} is not in a Git repository, so there is no subdirectory associated with it"> </span>
140 {% else %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500141 <a href="{% url 'layerdetails' extra.pid data.id %}">
142 <code>{{data.dirpath}}</code>
143 </a>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600144 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500145 {% if data.dirpath and data.get_vcs_dirpath_link_url %}
146 <a target="_blank" href="{{ data.get_vcs_dirpath_link_url }}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600147 <span class="glyphicon glyphicon-new-window"></span>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500148 </a>
149 {% endif %}'''
150
151 self.add_column(title="Subdirectory",
152 help_text="The layer directory within the Git repository",
153 hidden=True,
154 static_data_name="git_subdir",
155 static_data_template=git_dir_template)
156
157 revision_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600158 {% if data.layer.local_source_dir %}
159 <span class="text-muted">Not applicable</span>
160 <span class="glyphicon glyphicon-question-sign get-help" data-original-title="" title="The source code of {{data.layer.name}} is not in a Git repository, so there is no revision associated with it"> </span>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500161 {% else %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600162 {% with vcs_ref=data.get_vcs_reference %}
163 {% include 'snippets/gitrev_popover.html' %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500164 {% endwith %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600165 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500166 '''
167
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500168 self.add_column(title="Git revision",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500169 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",
170 static_data_name="revision",
171 static_data_template=revision_template)
172
173 deps_template = '''
174 {% with ods=data.dependencies.all%}
175 {% if ods.count %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600176 <a class="btn btn-default" title="<a href='{% url "layerdetails" extra.pid data.id %}'>{{data.layer.name}}</a> dependencies"
177 data-content="<ul class='list-unstyled'>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500178 {% for i in ods%}
179 <li><a href='{% url "layerdetails" extra.pid i.depends_on.pk %}'>{{i.depends_on.layer.name}}</a></li>
180 {% endfor %}
181 </ul>">
182 {{ods.count}}
183 </a>
184 {% endif %}
185 {% endwith %}
186 '''
187
188 self.add_column(title="Dependencies",
189 help_text="Other layers a layer depends upon",
190 static_data_name="dependencies",
191 static_data_template=deps_template)
192
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500193 self.add_column(title="Add | Remove",
194 help_text="Add or remove layers to / from your project",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500195 hideable=False,
196 filter_name="in_current_project",
197 static_data_name="add-del-layers",
198 static_data_template='{% include "layer_btn.html" %}')
199
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500200
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500201class MachinesTable(ToasterTable):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500202 """Table of Machines in Toaster"""
203
204 def __init__(self, *args, **kwargs):
205 super(MachinesTable, self).__init__(*args, **kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600206 self.empty_state = "Toaster has no machine information for this project. Sadly, machine information cannot be obtained from builds, so this page will remain empty."
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500207 self.title = "Compatible machines"
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500208 self.default_orderby = "name"
209
210 def get_context_data(self, **kwargs):
211 context = super(MachinesTable, self).get_context_data(**kwargs)
212 context['project'] = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500213 return context
214
215 def setup_filters(self, *args, **kwargs):
216 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500217
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500218 in_current_project_filter = TableFilter(
219 "in_current_project",
220 "Filter by project machines"
221 )
222
223 in_project_action = TableFilterActionToggle(
224 "in_project",
225 "Machines provided by layers added to this project",
226 ProjectFilters.in_project(self.project_layers)
227 )
228
229 not_in_project_action = TableFilterActionToggle(
230 "not_in_project",
231 "Machines provided by layers not added to this project",
232 ProjectFilters.not_in_project(self.project_layers)
233 )
234
235 in_current_project_filter.add_action(in_project_action)
236 in_current_project_filter.add_action(not_in_project_action)
237 self.add_filter(in_current_project_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500238
239 def setup_queryset(self, *args, **kwargs):
240 prj = Project.objects.get(pk = kwargs['pid'])
241 self.queryset = prj.get_all_compatible_machines()
242 self.queryset = self.queryset.order_by(self.default_orderby)
243
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500244 self.static_context_extra['current_layers'] = \
245 self.project_layers = \
246 prj.get_project_layer_versions(pk=True)
247
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500248 def setup_columns(self, *args, **kwargs):
249
250 self.add_column(title="Machine",
251 hideable=False,
252 orderable=True,
253 field_name="name")
254
255 self.add_column(title="Description",
256 field_name="description")
257
258 layer_link_template = '''
259 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
260 {{data.layer_version.layer.name}}</a>
261 '''
262
263 self.add_column(title="Layer",
264 static_data_name="layer_version__layer__name",
265 static_data_template=layer_link_template,
266 orderable=True)
267
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500268 self.add_column(title="Git revision",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500269 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",
270 hidden=True,
271 field_name="layer_version__get_vcs_reference")
272
273 machine_file_template = '''<code>conf/machine/{{data.name}}.conf</code>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600274 <a href="{{data.get_vcs_machine_file_link_url}}" target="_blank"><span class="glyphicon glyphicon-new-window"></i></a>'''
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500275
276 self.add_column(title="Machine file",
277 hidden=True,
278 static_data_name="machinefile",
279 static_data_template=machine_file_template)
280
281 self.add_column(title="Select",
282 help_text="Sets the selected machine as the project machine. You can only have one machine per project",
283 hideable=False,
284 filter_name="in_current_project",
285 static_data_name="add-del-layers",
286 static_data_template='{% include "machine_btn.html" %}')
287
288
289class LayerMachinesTable(MachinesTable):
290 """ Smaller version of the Machines table for use in layer details """
291
292 def __init__(self, *args, **kwargs):
293 super(LayerMachinesTable, self).__init__(*args, **kwargs)
294
295 def get_context_data(self, **kwargs):
296 context = super(LayerMachinesTable, self).get_context_data(**kwargs)
297 context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
298 return context
299
300
301 def setup_queryset(self, *args, **kwargs):
302 MachinesTable.setup_queryset(self, *args, **kwargs)
303
304 self.queryset = self.queryset.filter(layer_version__pk=int(kwargs['layerid']))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500305 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500306 self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
307
308 def setup_columns(self, *args, **kwargs):
309 self.add_column(title="Machine",
310 hideable=False,
311 orderable=True,
312 field_name="name")
313
314 self.add_column(title="Description",
315 field_name="description")
316
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600317 select_btn_template = '''
318 <a href="{% url "project" extra.pid %}?setMachine={{data.name}}"
319 class="btn btn-default btn-block select-machine-btn
320 {% if extra.in_prj == 0%}disabled{%endif%}">Select machine</a>
321 '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500322
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)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600333 self.empty_state = "Toaster has no recipe information. To generate recipe information you need to run a build."
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500334
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500335 build_col = { 'title' : "Build",
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600336 'help_text' : "Before building a recipe, you might need to add the corresponding layer to your project",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500337 '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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600347 context['projectlayers'] = [player.layercommit.id for player in ProjectLayer.objects.filter(project=context['project'])]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500348
349 return context
350
351 def setup_filters(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500352 table_filter = TableFilter(
353 'in_current_project',
354 'Filter by project recipes'
355 )
356
357 in_project_action = TableFilterActionToggle(
358 'in_project',
359 'Recipes provided by layers added to this project',
360 ProjectFilters.in_project(self.project_layers)
361 )
362
363 not_in_project_action = TableFilterActionToggle(
364 'not_in_project',
365 'Recipes provided by layers not added to this project',
366 ProjectFilters.not_in_project(self.project_layers)
367 )
368
369 table_filter.add_action(in_project_action)
370 table_filter.add_action(not_in_project_action)
371 self.add_filter(table_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500372
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500373 def setup_queryset(self, *args, **kwargs):
374 prj = Project.objects.get(pk = kwargs['pid'])
375
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500376 # Project layers used by the filters
377 self.project_layers = prj.get_project_layer_versions(pk=True)
378
379 # Project layers used to switch the button states
380 self.static_context_extra['current_layers'] = self.project_layers
381
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500382 self.queryset = prj.get_all_compatible_recipes()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500383
384
385 def setup_columns(self, *args, **kwargs):
386
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500387 self.add_column(title="Version",
388 hidden=False,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500389 field_name="version")
390
391 self.add_column(title="Description",
392 field_name="get_description_or_summary")
393
394 recipe_file_template = '''
395 <code>{{data.file_path}}</code>
396 <a href="{{data.get_vcs_recipe_file_link_url}}" target="_blank">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600397 <span class="glyphicon glyphicon-new-window"></i>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500398 </a>
399 '''
400
401 self.add_column(title="Recipe file",
402 help_text="Path to the recipe .bb file",
403 hidden=True,
404 static_data_name="recipe-file",
405 static_data_template=recipe_file_template)
406
407 self.add_column(title="Section",
408 help_text="The section in which recipes should be categorized",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500409 hidden=True,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500410 orderable=True,
411 field_name="section")
412
413 layer_link_template = '''
414 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
415 {{data.layer_version.layer.name}}</a>
416 '''
417
418 self.add_column(title="Layer",
419 help_text="The name of the layer providing the recipe",
420 orderable=True,
421 static_data_name="layer_version__layer__name",
422 static_data_template=layer_link_template)
423
424 self.add_column(title="License",
425 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 -0500426 hidden=True,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500427 orderable=True,
428 field_name="license")
429
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600430 revision_link_template = '''
431 {% if data.layer_version.layer.local_source_dir %}
432 <span class="text-muted">Not applicable</span>
433 <span class="glyphicon glyphicon-question-sign get-help" data-original-title="" title="The source code of {{data.layer_version.layer.name}} is not in a Git repository, so there is no revision associated with it"> </span>
434 {% else %}
435 {{data.layer_version.get_vcs_reference}}
436 {% endif %}
437 '''
438
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500439 self.add_column(title="Git revision",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500440 hidden=True,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600441 static_data_name="layer_version__get_vcs_reference",
442 static_data_template=revision_link_template)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500443
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500444
445class LayerRecipesTable(RecipesTable):
446 """ Smaller version of the Recipes table for use in layer details """
447
448 def __init__(self, *args, **kwargs):
449 super(LayerRecipesTable, self).__init__(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500450 self.default_orderby = "name"
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500451
452 def get_context_data(self, **kwargs):
453 context = super(LayerRecipesTable, self).get_context_data(**kwargs)
454 context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
455 return context
456
457
458 def setup_queryset(self, *args, **kwargs):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500459 self.queryset = \
460 Recipe.objects.filter(layer_version__pk=int(kwargs['layerid']))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500461
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500462 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500463 self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
464
465 def setup_columns(self, *args, **kwargs):
466 self.add_column(title="Recipe",
467 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",
468 hideable=False,
469 orderable=True,
470 field_name="name")
471
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500472 self.add_column(title="Version",
473 field_name="version")
474
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500475 self.add_column(title="Description",
476 field_name="get_description_or_summary")
477
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600478 build_recipe_template = '''
479 <a class="btn btn-default btn-block build-recipe-btn
480 {% if extra.in_prj == 0 %}disabled{% endif %}"
481 data-recipe-name="{{data.name}}">Build recipe</a>
482 '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500483
484 self.add_column(title="Build recipe",
485 static_data_name="add-del-layers",
486 static_data_template=build_recipe_template)
487
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500488class CustomImagesTable(ToasterTable):
489 """ Table to display your custom images """
490 def __init__(self, *args, **kwargs):
491 super(CustomImagesTable, self).__init__(*args, **kwargs)
492 self.title = "Custom images"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500493 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500494
495 def get_context_data(self, **kwargs):
496 context = super(CustomImagesTable, self).get_context_data(**kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600497
498 empty_state_template = '''
499 You have not created any custom images yet.
500 <a href="{% url 'newcustomimage' data.pid %}">
501 Create your first custom image</a>
502 '''
503 context['empty_state'] = self.render_static_data(empty_state_template,
504 kwargs)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500505 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600506
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500507 # TODO put project into the ToasterTable base class
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500508 context['project'] = project
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500509 return context
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500510
511 def setup_queryset(self, *args, **kwargs):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500512 prj = Project.objects.get(pk = kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500513 self.queryset = CustomImageRecipe.objects.filter(project=prj)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500514 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500515
516 def setup_columns(self, *args, **kwargs):
517
518 name_link_template = '''
519 <a href="{% url 'customrecipe' extra.pid data.id %}">
520 {{data.name}}
521 </a>
522 '''
523
524 self.add_column(title="Custom image",
525 hideable=False,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500526 orderable=True,
527 field_name="name",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500528 static_data_name="name",
529 static_data_template=name_link_template)
530
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500531 recipe_file_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600532 {% if data.get_base_recipe_file %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500533 <code>{{data.name}}_{{data.version}}.bb</code>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600534 <a href="{% url 'customrecipedownload' extra.pid data.pk %}"
535 class="glyphicon glyphicon-download-alt get-help" title="Download recipe file"></a>
536 {% endif %}'''
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500537
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500538 self.add_column(title="Recipe file",
539 static_data_name='recipe_file_download',
540 static_data_template=recipe_file_template)
541
542 approx_packages_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600543 {% if data.get_all_packages.count > 0 %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500544 <a href="{% url 'customrecipe' extra.pid data.id %}">
545 {{data.get_all_packages.count}}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600546 </a>
547 {% endif %}'''
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500548
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600549 self.add_column(title="Packages",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500550 static_data_name='approx_packages',
551 static_data_template=approx_packages_template)
552
553
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500554 build_btn_template = '''
555 <button data-recipe-name="{{data.name}}"
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600556 class="btn btn-default btn-block build-recipe-btn">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500557 Build
558 </button>'''
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500559
560 self.add_column(title="Build",
561 hideable=False,
562 static_data_name='build_custom_img',
563 static_data_template=build_btn_template)
564
565class ImageRecipesTable(RecipesTable):
566 """ A subset of the recipes table which displayed just image recipes """
567
568 def __init__(self, *args, **kwargs):
569 super(ImageRecipesTable, self).__init__(*args, **kwargs)
570 self.title = "Compatible image recipes"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500571 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500572
573 def setup_queryset(self, *args, **kwargs):
574 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
575
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500576 custom_image_recipes = CustomImageRecipe.objects.filter(
577 project=kwargs['pid'])
578 self.queryset = self.queryset.filter(
579 Q(is_image=True) & ~Q(pk__in=custom_image_recipes))
580 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500581
582
583 def setup_columns(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500584
585 name_link_template = '''
586 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
587 '''
588
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500589 self.add_column(title="Image recipe",
590 help_text="When you build an image recipe, you get an "
591 "image: a root file system you can"
592 "deploy to a machine",
593 hideable=False,
594 orderable=True,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500595 static_data_name="name",
596 static_data_template=name_link_template,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500597 field_name="name")
598
599 super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
600
601 self.add_column(**RecipesTable.build_col)
602
603
604class NewCustomImagesTable(ImageRecipesTable):
605 """ Table which displays Images recipes which can be customised """
606 def __init__(self, *args, **kwargs):
607 super(NewCustomImagesTable, self).__init__(*args, **kwargs)
608 self.title = "Select the image recipe you want to customise"
609
610 def setup_queryset(self, *args, **kwargs):
611 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500612 prj = Project.objects.get(pk = kwargs['pid'])
613 self.static_context_extra['current_layers'] = \
614 prj.get_project_layer_versions(pk=True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500615
616 self.queryset = self.queryset.filter(is_image=True)
617
618 def setup_columns(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500619
620 name_link_template = '''
621 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
622 '''
623
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500624 self.add_column(title="Image recipe",
625 help_text="When you build an image recipe, you get an "
626 "image: a root file system you can"
627 "deploy to a machine",
628 hideable=False,
629 orderable=True,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500630 static_data_name="name",
631 static_data_template=name_link_template,
632 field_name="name")
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500633
634 super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
635
636 self.add_column(title="Customise",
637 hideable=False,
638 filter_name="in_current_project",
639 static_data_name="customise-or-add-recipe",
640 static_data_template='{% include "customise_btn.html" %}')
641
642
643class SoftwareRecipesTable(RecipesTable):
644 """ Displays just the software recipes """
645 def __init__(self, *args, **kwargs):
646 super(SoftwareRecipesTable, self).__init__(*args, **kwargs)
647 self.title = "Compatible software recipes"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500648 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500649
650 def setup_queryset(self, *args, **kwargs):
651 super(SoftwareRecipesTable, self).setup_queryset(*args, **kwargs)
652
653 self.queryset = self.queryset.filter(is_image=False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500654 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500655
656
657 def setup_columns(self, *args, **kwargs):
658 self.add_column(title="Software recipe",
659 help_text="Information about a single piece of "
660 "software, including where to download the source, "
661 "configuration options, how to compile the source "
662 "files and how to package the compiled output",
663 hideable=False,
664 orderable=True,
665 field_name="name")
666
667 super(SoftwareRecipesTable, self).setup_columns(*args, **kwargs)
668
669 self.add_column(**RecipesTable.build_col)
670
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500671class PackagesTable(ToasterTable):
672 """ Table to display the packages in a recipe from it's last successful
673 build"""
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500674
675 def __init__(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500676 super(PackagesTable, self).__init__(*args, **kwargs)
677 self.title = "Packages included"
678 self.packages = None
679 self.default_orderby = "name"
680
681 def create_package_list(self, recipe, project_id):
682 """Creates a list of packages for the specified recipe by looking for
683 the last SUCCEEDED build of ther recipe"""
684
685 target = Target.objects.filter(Q(target=recipe.name) &
686 Q(build__project_id=project_id) &
687 Q(build__outcome=Build.SUCCEEDED)
688 ).last()
689
690 if target:
691 pkgs = target.target_installed_package_set.values_list('package',
692 flat=True)
693 return Package.objects.filter(pk__in=pkgs)
694
695 # Target/recipe never successfully built so empty queryset
696 return Package.objects.none()
697
698 def get_context_data(self, **kwargs):
699 """Context for rendering the sidebar and other items on the recipe
700 details page """
701 context = super(PackagesTable, self).get_context_data(**kwargs)
702
703 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
704 project = Project.objects.get(pk=kwargs['pid'])
705
706 in_project = (recipe.layer_version.pk in
707 project.get_project_layer_versions(pk=True))
708
709 packages = self.create_package_list(recipe, project.pk)
710
711 context.update({'project': project,
712 'recipe' : recipe,
713 'packages': packages,
714 'approx_pkg_size' : packages.aggregate(Sum('size')),
715 'in_project' : in_project,
716 })
717
718 return context
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500719
720 def setup_queryset(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500721 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600722 self.static_context_extra['target_name'] = recipe.name
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500723
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500724 self.queryset = self.create_package_list(recipe, kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500725 self.queryset = self.queryset.order_by('name')
726
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500727 def setup_columns(self, *args, **kwargs):
728 self.add_column(title="Package",
729 hideable=False,
730 orderable=True,
731 field_name="name")
732
733 self.add_column(title="Package Version",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500734 field_name="version",
735 hideable=False)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500736
737 self.add_column(title="Approx Size",
738 orderable=True,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600739 field_name="size",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500740 static_data_name="size",
741 static_data_template="{% load projecttags %} \
742 {{data.size|filtered_filesizeformat}}")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500743
744 self.add_column(title="License",
745 field_name="license",
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600746 orderable=True,
747 hidden=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500748
749
750 self.add_column(title="Dependencies",
751 static_data_name="dependencies",
752 static_data_template='\
753 {% include "snippets/pkg_dependencies_popover.html" %}')
754
755 self.add_column(title="Reverse dependencies",
756 static_data_name="reverse_dependencies",
757 static_data_template='\
758 {% include "snippets/pkg_revdependencies_popover.html" %}',
759 hidden=True)
760
761 self.add_column(title="Recipe",
762 field_name="recipe__name",
763 orderable=True,
764 hidden=True)
765
766 self.add_column(title="Recipe version",
767 field_name="recipe__version",
768 hidden=True)
769
770
771class SelectPackagesTable(PackagesTable):
772 """ Table to display the packages to add and remove from an image """
773
774 def __init__(self, *args, **kwargs):
775 super(SelectPackagesTable, self).__init__(*args, **kwargs)
776 self.title = "Add | Remove packages"
777
778 def setup_queryset(self, *args, **kwargs):
779 self.cust_recipe =\
780 CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
781 prj = Project.objects.get(pk = kwargs['pid'])
782
783 current_packages = self.cust_recipe.get_all_packages()
784
785 current_recipes = prj.get_available_recipes()
786
787 # only show packages where recipes->layers are in the project
788 self.queryset = CustomImagePackage.objects.filter(
789 ~Q(recipe=None) &
790 Q(recipe__in=current_recipes))
791
792 self.queryset = self.queryset.order_by('name')
793
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600794 # This target is the target used to work out which group of dependences
795 # to display, if we've built the custom image we use it otherwise we
796 # can use the based recipe instead
797 if prj.build_set.filter(target__target=self.cust_recipe.name).count()\
798 > 0:
799 self.static_context_extra['target_name'] = self.cust_recipe.name
800 else:
801 self.static_context_extra['target_name'] =\
802 Package_DependencyManager.TARGET_LATEST
803
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500804 self.static_context_extra['recipe_id'] = kwargs['custrecipeid']
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600805
806
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500807 self.static_context_extra['current_packages'] = \
808 current_packages.values_list('pk', flat=True)
809
810 def get_context_data(self, **kwargs):
811 # to reuse the Super class map the custrecipeid to the recipe_id
812 kwargs['recipe_id'] = kwargs['custrecipeid']
813 context = super(SelectPackagesTable, self).get_context_data(**kwargs)
814 custom_recipe = \
815 CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
816
817 context['recipe'] = custom_recipe
818 context['approx_pkg_size'] = \
819 custom_recipe.get_all_packages().aggregate(Sum('size'))
820 return context
821
822
823 def setup_columns(self, *args, **kwargs):
824 super(SelectPackagesTable, self).setup_columns(*args, **kwargs)
825
826 add_remove_template = '{% include "pkg_add_rm_btn.html" %}'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500827
828 self.add_column(title="Add | Remove",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500829 hideable=False,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500830 help_text="Use the add and remove buttons to modify "
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500831 "the package content of your custom image",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500832 static_data_name="add_rm_pkg_btn",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500833 static_data_template=add_remove_template,
834 filter_name='in_current_image_filter')
835
836 def setup_filters(self, *args, **kwargs):
837 in_current_image_filter = TableFilter(
838 'in_current_image_filter',
839 'Filter by added packages'
840 )
841
842 in_image_action = TableFilterActionToggle(
843 'in_image',
844 'Packages in %s' % self.cust_recipe.name,
845 Q(pk__in=self.static_context_extra['current_packages'])
846 )
847
848 not_in_image_action = TableFilterActionToggle(
849 'not_in_image',
850 'Packages not added to %s' % self.cust_recipe.name,
851 ~Q(pk__in=self.static_context_extra['current_packages'])
852 )
853
854 in_current_image_filter.add_action(in_image_action)
855 in_current_image_filter.add_action(not_in_image_action)
856 self.add_filter(in_current_image_filter)
857
858class ProjectsTable(ToasterTable):
859 """Table of projects in Toaster"""
860
861 def __init__(self, *args, **kwargs):
862 super(ProjectsTable, self).__init__(*args, **kwargs)
863 self.default_orderby = '-updated'
864 self.title = 'All projects'
865 self.static_context_extra['Build'] = Build
866
867 def get_context_data(self, **kwargs):
868 return super(ProjectsTable, self).get_context_data(**kwargs)
869
870 def setup_queryset(self, *args, **kwargs):
871 queryset = Project.objects.all()
872
873 # annotate each project with its number of builds
874 queryset = queryset.annotate(num_builds=Count('build'))
875
876 # exclude the command line builds project if it has no builds
877 q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0)
878 queryset = queryset.filter(Q(is_default=False) |
879 q_default_with_builds)
880
881 # order rows
882 queryset = queryset.order_by(self.default_orderby)
883
884 self.queryset = queryset
885
886 # columns: last activity on (updated) - DEFAULT, project (name), release,
887 # machine, number of builds, last build outcome, recipe (name), errors,
888 # warnings, image files
889 def setup_columns(self, *args, **kwargs):
890 name_template = '''
891 {% load project_url_tag %}
892 <span data-project-field="name">
893 <a href="{% project_url data %}">
894 {{data.name}}
895 </a>
896 </span>
897 '''
898
899 last_activity_on_template = '''
900 {% load project_url_tag %}
901 <span data-project-field="updated">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500902 {{data.updated | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500903 </span>
904 '''
905
906 release_template = '''
907 <span data-project-field="release">
908 {% if data.release %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600909 {{data.release.name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500910 {% elif data.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600911 <span class="text-muted">Not applicable</span>
912 <span class="glyphicon glyphicon-question-sign get-help hover-help"
913 title="This project does not have a release set.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500914 It simply collects information about the builds you start from
915 the command line while Toaster is running"
916 style="visibility: hidden;">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600917 </span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500918 {% else %}
919 No release available
920 {% endif %}
921 </span>
922 '''
923
924 machine_template = '''
925 <span data-project-field="machine">
926 {% if data.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600927 <span class="text-muted">Not applicable</span>
928 <span class="glyphicon glyphicon-question-sign get-help hover-help"
929 title="This project does not have a machine
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500930 set. It simply collects information about the builds you
931 start from the command line while Toaster is running"
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600932 style="visibility: hidden;"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500933 {% else %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600934 {{data.get_current_machine_name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500935 {% endif %}
936 </span>
937 '''
938
939 number_of_builds_template = '''
940 {% if data.get_number_of_builds > 0 %}
941 <a href="{% url 'projectbuilds' data.id %}">
942 {{data.get_number_of_builds}}
943 </a>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500944 {% endif %}
945 '''
946
947 last_build_outcome_template = '''
948 {% if data.get_number_of_builds > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600949 {% if data.get_last_outcome == extra.Build.SUCCEEDED %}
950 <span class="glyphicon glyphicon-ok-circle"></span>
951 {% elif data.get_last_outcome == extra.Build.FAILED %}
952 <span class="glyphicon glyphicon-minus-sign"></span>
953 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500954 {% endif %}
955 '''
956
957 recipe_template = '''
958 {% if data.get_number_of_builds > 0 %}
959 <a href="{% url "builddashboard" data.get_last_build_id %}">
960 {{data.get_last_target}}
961 </a>
962 {% endif %}
963 '''
964
965 errors_template = '''
966 {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600967 <a class="errors.count text-danger"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500968 href="{% url "builddashboard" data.get_last_build_id %}#errors">
969 {{data.get_last_errors}} error{{data.get_last_errors | pluralize}}
970 </a>
971 {% endif %}
972 '''
973
974 warnings_template = '''
975 {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600976 <a class="warnings.count text-warning"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500977 href="{% url "builddashboard" data.get_last_build_id %}#warnings">
978 {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}}
979 </a>
980 {% endif %}
981 '''
982
983 image_files_template = '''
984 {% if data.get_number_of_builds > 0 and data.get_last_outcome == extra.Build.SUCCEEDED %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600985 {{data.get_last_build_extensions}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500986 {% endif %}
987 '''
988
989 self.add_column(title='Project',
990 hideable=False,
991 orderable=True,
992 static_data_name='name',
993 static_data_template=name_template)
994
995 self.add_column(title='Last activity on',
996 help_text='Starting date and time of the \
997 last project build. If the project has no \
998 builds, this shows the date the project was \
999 created.',
1000 hideable=False,
1001 orderable=True,
1002 static_data_name='updated',
1003 static_data_template=last_activity_on_template)
1004
1005 self.add_column(title='Release',
1006 help_text='The version of the build system used by \
1007 the project',
1008 hideable=False,
1009 orderable=True,
1010 static_data_name='release',
1011 static_data_template=release_template)
1012
1013 self.add_column(title='Machine',
1014 help_text='The hardware currently selected for the \
1015 project',
1016 hideable=False,
1017 orderable=False,
1018 static_data_name='machine',
1019 static_data_template=machine_template)
1020
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001021 self.add_column(title='Builds',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001022 help_text='The number of builds which have been run \
1023 for the project',
1024 hideable=False,
1025 orderable=False,
1026 static_data_name='number_of_builds',
1027 static_data_template=number_of_builds_template)
1028
1029 self.add_column(title='Last build outcome',
1030 help_text='Indicates whether the last project build \
1031 completed successfully or failed',
1032 hideable=True,
1033 orderable=False,
1034 static_data_name='last_build_outcome',
1035 static_data_template=last_build_outcome_template)
1036
1037 self.add_column(title='Recipe',
1038 help_text='The last recipe which was built in this \
1039 project',
1040 hideable=True,
1041 orderable=False,
1042 static_data_name='recipe_name',
1043 static_data_template=recipe_template)
1044
1045 self.add_column(title='Errors',
1046 help_text='The number of errors encountered during \
1047 the last project build (if any)',
1048 hideable=True,
1049 orderable=False,
1050 static_data_name='errors',
1051 static_data_template=errors_template)
1052
1053 self.add_column(title='Warnings',
1054 help_text='The number of warnings encountered during \
1055 the last project build (if any)',
1056 hideable=True,
1057 hidden=True,
1058 orderable=False,
1059 static_data_name='warnings',
1060 static_data_template=warnings_template)
1061
1062 self.add_column(title='Image files',
1063 help_text='The root file system types produced by \
1064 the last project build',
1065 hideable=True,
1066 hidden=True,
1067 orderable=False,
1068 static_data_name='image_files',
1069 static_data_template=image_files_template)
1070
1071class BuildsTable(ToasterTable):
1072 """Table of builds in Toaster"""
1073
1074 def __init__(self, *args, **kwargs):
1075 super(BuildsTable, self).__init__(*args, **kwargs)
1076 self.default_orderby = '-completed_on'
1077 self.static_context_extra['Build'] = Build
1078 self.static_context_extra['Task'] = Task
1079
1080 # attributes that are overridden in subclasses
1081
1082 # title for the page
1083 self.title = ''
1084
1085 # 'project' or 'all'; determines how the mrb (most recent builds)
1086 # section is displayed
1087 self.mrb_type = ''
1088
1089 def get_builds(self):
1090 """
1091 overridden in ProjectBuildsTable to return builds for a
1092 single project
1093 """
1094 return Build.objects.all()
1095
1096 def get_context_data(self, **kwargs):
1097 context = super(BuildsTable, self).get_context_data(**kwargs)
1098
1099 # should be set in subclasses
1100 context['mru'] = []
1101
1102 context['mrb_type'] = self.mrb_type
1103
1104 return context
1105
1106 def setup_queryset(self, *args, **kwargs):
1107 """
1108 The queryset is annotated so that it can be sorted by number of
1109 errors and number of warnings; but note that the criteria for
1110 finding the log messages to populate these fields should match those
1111 used in the Build model (orm/models.py) to populate the errors and
1112 warnings properties
1113 """
1114 queryset = self.get_builds()
1115
1116 # Don't include in progress builds pr cancelled builds
1117 queryset = queryset.exclude(Q(outcome=Build.IN_PROGRESS) |
1118 Q(outcome=Build.CANCELLED))
1119
1120 # sort
1121 queryset = queryset.order_by(self.default_orderby)
1122
1123 # annotate with number of ERROR, EXCEPTION and CRITICAL log messages
1124 criteria = (Q(logmessage__level=LogMessage.ERROR) |
1125 Q(logmessage__level=LogMessage.EXCEPTION) |
1126 Q(logmessage__level=LogMessage.CRITICAL))
1127
1128 queryset = queryset.annotate(
1129 errors_no=Count(
1130 Case(
1131 When(criteria, then=Value(1)),
1132 output_field=IntegerField()
1133 )
1134 )
1135 )
1136
1137 # annotate with number of WARNING log messages
1138 queryset = queryset.annotate(
1139 warnings_no=Count(
1140 Case(
1141 When(logmessage__level=LogMessage.WARNING, then=Value(1)),
1142 output_field=IntegerField()
1143 )
1144 )
1145 )
1146
1147 self.queryset = queryset
1148
1149 def setup_columns(self, *args, **kwargs):
1150 outcome_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001151 {% if data.outcome == data.SUCCEEDED %}
1152 <span class="glyphicon glyphicon-ok-circle"></span>
1153 {% elif data.outcome == data.FAILED %}
1154 <span class="glyphicon glyphicon-minus-sign"></span>
1155 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001156
1157 {% if data.cooker_log_path %}
1158 &nbsp;
1159 <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001160 <span class="glyphicon glyphicon-download-alt get-help"
1161 data-original-title="Download build log"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001162 </a>
1163 {% endif %}
1164 '''
1165
1166 recipe_template = '''
1167 {% for target_label in data.target_labels %}
1168 <a href="{% url "builddashboard" data.id %}">
1169 {{target_label}}
1170 </a>
1171 <br />
1172 {% endfor %}
1173 '''
1174
1175 machine_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001176 {{data.machine}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001177 '''
1178
1179 started_on_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001180 {{data.started_on | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001181 '''
1182
1183 completed_on_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001184 {{data.completed_on | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001185 '''
1186
1187 failed_tasks_template = '''
1188 {% if data.failed_tasks.count == 1 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001189 <a class="text-danger" href="{% url "task" data.id data.failed_tasks.0.id %}">
1190 <span>
1191 {{data.failed_tasks.0.recipe.name}} {{data.failed_tasks.0.task_name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001192 </span>
1193 </a>
1194 <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001195 <span class="glyphicon glyphicon-download-alt get-help"
1196 title="Download task log file">
1197 </span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001198 </a>
1199 {% elif data.failed_tasks.count > 1 %}
1200 <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001201 <span class="text-danger">{{data.failed_tasks.count}} tasks</span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001202 </a>
1203 {% endif %}
1204 '''
1205
1206 errors_template = '''
1207 {% if data.errors_no %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001208 <a class="errors.count text-danger" href="{% url "builddashboard" data.id %}#errors">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001209 {{data.errors_no}} error{{data.errors_no|pluralize}}
1210 </a>
1211 {% endif %}
1212 '''
1213
1214 warnings_template = '''
1215 {% if data.warnings_no %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001216 <a class="warnings.count text-warning" href="{% url "builddashboard" data.id %}#warnings">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001217 {{data.warnings_no}} warning{{data.warnings_no|pluralize}}
1218 </a>
1219 {% endif %}
1220 '''
1221
1222 time_template = '''
1223 {% load projecttags %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001224 {% if data.outcome == extra.Build.SUCCEEDED %}
1225 <a href="{% url "buildtime" data.id %}">
1226 {{data.timespent_seconds | sectohms}}
1227 </a>
1228 {% else %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001229 {{data.timespent_seconds | sectohms}}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001230 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001231 '''
1232
1233 image_files_template = '''
1234 {% if data.outcome == extra.Build.SUCCEEDED %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001235 {{data.get_image_file_extensions}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001236 {% endif %}
1237 '''
1238
1239 self.add_column(title='Outcome',
1240 help_text='Final state of the build (successful \
1241 or failed)',
1242 hideable=False,
1243 orderable=True,
1244 filter_name='outcome_filter',
1245 static_data_name='outcome',
1246 static_data_template=outcome_template)
1247
1248 self.add_column(title='Recipe',
1249 help_text='What was built (i.e. one or more recipes \
1250 or image recipes)',
1251 hideable=False,
1252 orderable=False,
1253 static_data_name='target',
1254 static_data_template=recipe_template)
1255
1256 self.add_column(title='Machine',
1257 help_text='Hardware for which you are building a \
1258 recipe or image recipe',
1259 hideable=False,
1260 orderable=True,
1261 static_data_name='machine',
1262 static_data_template=machine_template)
1263
1264 self.add_column(title='Started on',
1265 help_text='The date and time when the build started',
1266 hideable=True,
1267 hidden=True,
1268 orderable=True,
1269 filter_name='started_on_filter',
1270 static_data_name='started_on',
1271 static_data_template=started_on_template)
1272
1273 self.add_column(title='Completed on',
1274 help_text='The date and time when the build finished',
1275 hideable=False,
1276 orderable=True,
1277 filter_name='completed_on_filter',
1278 static_data_name='completed_on',
1279 static_data_template=completed_on_template)
1280
1281 self.add_column(title='Failed tasks',
1282 help_text='The number of tasks which failed during \
1283 the build',
1284 hideable=True,
1285 orderable=False,
1286 filter_name='failed_tasks_filter',
1287 static_data_name='failed_tasks',
1288 static_data_template=failed_tasks_template)
1289
1290 self.add_column(title='Errors',
1291 help_text='The number of errors encountered during \
1292 the build (if any)',
1293 hideable=True,
1294 orderable=True,
1295 static_data_name='errors_no',
1296 static_data_template=errors_template)
1297
1298 self.add_column(title='Warnings',
1299 help_text='The number of warnings encountered during \
1300 the build (if any)',
1301 hideable=True,
1302 orderable=True,
1303 static_data_name='warnings_no',
1304 static_data_template=warnings_template)
1305
1306 self.add_column(title='Time',
1307 help_text='How long the build took to finish',
1308 hideable=True,
1309 hidden=True,
1310 orderable=False,
1311 static_data_name='time',
1312 static_data_template=time_template)
1313
1314 self.add_column(title='Image files',
1315 help_text='The root file system types produced by \
1316 the build',
1317 hideable=True,
1318 orderable=False,
1319 static_data_name='image_files',
1320 static_data_template=image_files_template)
1321
1322 def setup_filters(self, *args, **kwargs):
1323 # outcomes
1324 outcome_filter = TableFilter(
1325 'outcome_filter',
1326 'Filter builds by outcome'
1327 )
1328
1329 successful_builds_action = TableFilterActionToggle(
1330 'successful_builds',
1331 'Successful builds',
1332 Q(outcome=Build.SUCCEEDED)
1333 )
1334
1335 failed_builds_action = TableFilterActionToggle(
1336 'failed_builds',
1337 'Failed builds',
1338 Q(outcome=Build.FAILED)
1339 )
1340
1341 outcome_filter.add_action(successful_builds_action)
1342 outcome_filter.add_action(failed_builds_action)
1343 self.add_filter(outcome_filter)
1344
1345 # started on
1346 started_on_filter = TableFilter(
1347 'started_on_filter',
1348 'Filter by date when build was started'
1349 )
1350
1351 started_today_action = TableFilterActionDay(
1352 'today',
1353 'Today\'s builds',
1354 'started_on',
1355 'today'
1356 )
1357
1358 started_yesterday_action = TableFilterActionDay(
1359 'yesterday',
1360 'Yesterday\'s builds',
1361 'started_on',
1362 'yesterday'
1363 )
1364
1365 by_started_date_range_action = TableFilterActionDateRange(
1366 'date_range',
1367 'Build date range',
1368 'started_on'
1369 )
1370
1371 started_on_filter.add_action(started_today_action)
1372 started_on_filter.add_action(started_yesterday_action)
1373 started_on_filter.add_action(by_started_date_range_action)
1374 self.add_filter(started_on_filter)
1375
1376 # completed on
1377 completed_on_filter = TableFilter(
1378 'completed_on_filter',
1379 'Filter by date when build was completed'
1380 )
1381
1382 completed_today_action = TableFilterActionDay(
1383 'today',
1384 'Today\'s builds',
1385 'completed_on',
1386 'today'
1387 )
1388
1389 completed_yesterday_action = TableFilterActionDay(
1390 'yesterday',
1391 'Yesterday\'s builds',
1392 'completed_on',
1393 'yesterday'
1394 )
1395
1396 by_completed_date_range_action = TableFilterActionDateRange(
1397 'date_range',
1398 'Build date range',
1399 'completed_on'
1400 )
1401
1402 completed_on_filter.add_action(completed_today_action)
1403 completed_on_filter.add_action(completed_yesterday_action)
1404 completed_on_filter.add_action(by_completed_date_range_action)
1405 self.add_filter(completed_on_filter)
1406
1407 # failed tasks
1408 failed_tasks_filter = TableFilter(
1409 'failed_tasks_filter',
1410 'Filter builds by failed tasks'
1411 )
1412
1413 criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
1414
1415 with_failed_tasks_action = TableFilterActionToggle(
1416 'with_failed_tasks',
1417 'Builds with failed tasks',
1418 criteria
1419 )
1420
1421 without_failed_tasks_action = TableFilterActionToggle(
1422 'without_failed_tasks',
1423 'Builds without failed tasks',
1424 ~criteria
1425 )
1426
1427 failed_tasks_filter.add_action(with_failed_tasks_action)
1428 failed_tasks_filter.add_action(without_failed_tasks_action)
1429 self.add_filter(failed_tasks_filter)
1430
1431
1432class AllBuildsTable(BuildsTable):
1433 """ Builds page for all builds """
1434
1435 def __init__(self, *args, **kwargs):
1436 super(AllBuildsTable, self).__init__(*args, **kwargs)
1437 self.title = 'All builds'
1438 self.mrb_type = 'all'
1439
1440 def setup_columns(self, *args, **kwargs):
1441 """
1442 All builds page shows a column for the project
1443 """
1444
1445 super(AllBuildsTable, self).setup_columns(*args, **kwargs)
1446
1447 project_template = '''
1448 {% load project_url_tag %}
1449 <a href="{% project_url data.project %}">
1450 {{data.project.name}}
1451 </a>
1452 {% if data.project.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001453 <span class="glyphicon glyphicon-question-sign get-help hover-help" title=""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001454 data-original-title="This project shows information about
1455 the builds you start from the command line while Toaster is
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001456 running" style="visibility: hidden;"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001457 {% endif %}
1458 '''
1459
1460 self.add_column(title='Project',
1461 hideable=True,
1462 orderable=True,
1463 static_data_name='project',
1464 static_data_template=project_template)
1465
1466 def get_context_data(self, **kwargs):
1467 """ Get all builds for the recent builds area """
1468 context = super(AllBuildsTable, self).get_context_data(**kwargs)
1469 context['mru'] = Build.get_recent()
1470 return context
1471
1472class ProjectBuildsTable(BuildsTable):
1473 """
1474 Builds page for a single project; a BuildsTable, with the queryset
1475 filtered by project
1476 """
1477
1478 def __init__(self, *args, **kwargs):
1479 super(ProjectBuildsTable, self).__init__(*args, **kwargs)
1480 self.title = 'All project builds'
1481 self.mrb_type = 'project'
1482
1483 # set from the querystring
1484 self.project_id = None
1485
1486 def setup_columns(self, *args, **kwargs):
1487 """
1488 Project builds table doesn't show the machines column by default
1489 """
1490
1491 super(ProjectBuildsTable, self).setup_columns(*args, **kwargs)
1492
1493 # hide the machine column
1494 self.set_column_hidden('Machine', True)
1495
1496 # allow the machine column to be hidden by the user
1497 self.set_column_hideable('Machine', True)
1498
1499 def setup_queryset(self, *args, **kwargs):
1500 """
1501 NOTE: self.project_id must be set before calling super(),
1502 as it's used in setup_queryset()
1503 """
1504 self.project_id = kwargs['pid']
1505 super(ProjectBuildsTable, self).setup_queryset(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001506 project = Project.objects.get(pk=self.project_id)
1507 self.queryset = self.queryset.filter(project=project)
1508
1509 def get_context_data(self, **kwargs):
1510 """
1511 Get recent builds for this project, and the project itself
1512
1513 NOTE: self.project_id must be set before calling super(),
1514 as it's used in get_context_data()
1515 """
1516 self.project_id = kwargs['pid']
1517 context = super(ProjectBuildsTable, self).get_context_data(**kwargs)
1518
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001519 empty_state_template = '''
1520 This project has no builds.
1521 <a href="{% url 'projectimagerecipes' data.pid %}">
1522 Choose a recipe to build</a>
1523 '''
1524 context['empty_state'] = self.render_static_data(empty_state_template,
1525 kwargs)
1526
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001527 project = Project.objects.get(pk=self.project_id)
1528 context['mru'] = Build.get_recent(project)
1529 context['project'] = project
1530
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001531 self.setup_queryset(**kwargs)
1532 if self.queryset.count() == 0 and \
1533 project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0:
1534 context['build_in_progress_none_completed'] = True
1535 else:
1536 context['build_in_progress_none_completed'] = False
1537
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001538 return context