blob: dca2fa29130467c4048d9dcaca228476ea1eaa19 [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
Brad Bishopd7bf8c12018-02-25 22:55:05 -050026from orm.models import Distro
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050027from django.db.models import Q, Max, Sum, Count, When, Case, Value, IntegerField
Patrick Williamsc124f4f2015-09-15 14:41:29 -050028from django.conf.urls import url
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050029from django.core.urlresolvers import reverse, resolve
30from django.http import HttpResponse
Patrick Williamsc124f4f2015-09-15 14:41:29 -050031from django.views.generic import TemplateView
32
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050033from toastergui.tablefilter import TableFilter
34from toastergui.tablefilter import TableFilterActionToggle
35from toastergui.tablefilter import TableFilterActionDateRange
36from toastergui.tablefilter import TableFilterActionDay
Patrick Williamsc124f4f2015-09-15 14:41:29 -050037
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050038class ProjectFilters(object):
39 @staticmethod
40 def in_project(project_layers):
41 return Q(layer_version__in=project_layers)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050042
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050043 @staticmethod
44 def not_in_project(project_layers):
45 return ~(ProjectFilters.in_project(project_layers))
Patrick Williamsc124f4f2015-09-15 14:41:29 -050046
47class LayersTable(ToasterTable):
48 """Table of layers in Toaster"""
49
50 def __init__(self, *args, **kwargs):
51 super(LayersTable, self).__init__(*args, **kwargs)
52 self.default_orderby = "layer__name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -050053 self.title = "Compatible layers"
Patrick Williamsc124f4f2015-09-15 14:41:29 -050054
55 def get_context_data(self, **kwargs):
56 context = super(LayersTable, self).get_context_data(**kwargs)
57
58 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -050059 context['project'] = project
Patrick Williamsc124f4f2015-09-15 14:41:29 -050060
61 return context
62
Patrick Williamsc124f4f2015-09-15 14:41:29 -050063 def setup_filters(self, *args, **kwargs):
64 project = Project.objects.get(pk=kwargs['pid'])
65 self.project_layers = ProjectLayer.objects.filter(project=project)
66
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050067 in_current_project_filter = TableFilter(
68 "in_current_project",
69 "Filter by project layers"
70 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050071
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050072 criteria = Q(projectlayer__in=self.project_layers)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050073
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050074 in_project_action = TableFilterActionToggle(
75 "in_project",
76 "Layers added to this project",
77 criteria
78 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050079
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050080 not_in_project_action = TableFilterActionToggle(
81 "not_in_project",
82 "Layers not added to this project",
83 ~criteria
84 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050085
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050086 in_current_project_filter.add_action(in_project_action)
87 in_current_project_filter.add_action(not_in_project_action)
88 self.add_filter(in_current_project_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050089
90 def setup_queryset(self, *args, **kwargs):
91 prj = Project.objects.get(pk = kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -050092 compatible_layers = prj.get_all_compatible_layer_versions()
93
94 self.static_context_extra['current_layers'] = \
95 prj.get_project_layer_versions(pk=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050096
97 self.queryset = compatible_layers.order_by(self.default_orderby)
98
99 def setup_columns(self, *args, **kwargs):
100
101 layer_link_template = '''
102 <a href="{% url 'layerdetails' extra.pid data.id %}">
103 {{data.layer.name}}
104 </a>
105 '''
106
107 self.add_column(title="Layer",
108 hideable=False,
109 orderable=True,
110 static_data_name="layer__name",
111 static_data_template=layer_link_template)
112
113 self.add_column(title="Summary",
114 field_name="layer__summary")
115
116 git_url_template = '''
117 <a href="{% url 'layerdetails' extra.pid data.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600118 {% if data.layer.local_source_dir %}
119 <code>{{data.layer.local_source_dir}}</code>
120 {% else %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500121 <code>{{data.layer.vcs_url}}</code>
122 </a>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600123 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500124 {% if data.get_vcs_link_url %}
125 <a target="_blank" href="{{ data.get_vcs_link_url }}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600126 <span class="glyphicon glyphicon-new-window"></span>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500127 </a>
128 {% endif %}
129 '''
130
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600131 self.add_column(title="Layer source code location",
132 help_text="A Git repository or an absolute path to a directory",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500133 hidden=True,
134 static_data_name="layer__vcs_url",
135 static_data_template=git_url_template)
136
137 git_dir_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600138 {% if data.layer.local_source_dir %}
139 <span class="text-muted">Not applicable</span>
140 <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>
141 {% else %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500142 <a href="{% url 'layerdetails' extra.pid data.id %}">
143 <code>{{data.dirpath}}</code>
144 </a>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600145 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500146 {% if data.dirpath and data.get_vcs_dirpath_link_url %}
147 <a target="_blank" href="{{ data.get_vcs_dirpath_link_url }}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600148 <span class="glyphicon glyphicon-new-window"></span>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500149 </a>
150 {% endif %}'''
151
152 self.add_column(title="Subdirectory",
153 help_text="The layer directory within the Git repository",
154 hidden=True,
155 static_data_name="git_subdir",
156 static_data_template=git_dir_template)
157
158 revision_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600159 {% if data.layer.local_source_dir %}
160 <span class="text-muted">Not applicable</span>
161 <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 -0500162 {% else %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600163 {% with vcs_ref=data.get_vcs_reference %}
164 {% include 'snippets/gitrev_popover.html' %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500165 {% endwith %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600166 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500167 '''
168
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500169 self.add_column(title="Git revision",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500170 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",
171 static_data_name="revision",
172 static_data_template=revision_template)
173
174 deps_template = '''
175 {% with ods=data.dependencies.all%}
176 {% if ods.count %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600177 <a class="btn btn-default" title="<a href='{% url "layerdetails" extra.pid data.id %}'>{{data.layer.name}}</a> dependencies"
178 data-content="<ul class='list-unstyled'>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500179 {% for i in ods%}
180 <li><a href='{% url "layerdetails" extra.pid i.depends_on.pk %}'>{{i.depends_on.layer.name}}</a></li>
181 {% endfor %}
182 </ul>">
183 {{ods.count}}
184 </a>
185 {% endif %}
186 {% endwith %}
187 '''
188
189 self.add_column(title="Dependencies",
190 help_text="Other layers a layer depends upon",
191 static_data_name="dependencies",
192 static_data_template=deps_template)
193
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500194 self.add_column(title="Add | Remove",
195 help_text="Add or remove layers to / from your project",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500196 hideable=False,
197 filter_name="in_current_project",
198 static_data_name="add-del-layers",
199 static_data_template='{% include "layer_btn.html" %}')
200
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500201
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500202class MachinesTable(ToasterTable):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500203 """Table of Machines in Toaster"""
204
205 def __init__(self, *args, **kwargs):
206 super(MachinesTable, self).__init__(*args, **kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600207 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 -0500208 self.title = "Compatible machines"
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500209 self.default_orderby = "name"
210
211 def get_context_data(self, **kwargs):
212 context = super(MachinesTable, self).get_context_data(**kwargs)
213 context['project'] = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500214 return context
215
216 def setup_filters(self, *args, **kwargs):
217 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500218
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500219 in_current_project_filter = TableFilter(
220 "in_current_project",
221 "Filter by project machines"
222 )
223
224 in_project_action = TableFilterActionToggle(
225 "in_project",
226 "Machines provided by layers added to this project",
227 ProjectFilters.in_project(self.project_layers)
228 )
229
230 not_in_project_action = TableFilterActionToggle(
231 "not_in_project",
232 "Machines provided by layers not added to this project",
233 ProjectFilters.not_in_project(self.project_layers)
234 )
235
236 in_current_project_filter.add_action(in_project_action)
237 in_current_project_filter.add_action(not_in_project_action)
238 self.add_filter(in_current_project_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500239
240 def setup_queryset(self, *args, **kwargs):
241 prj = Project.objects.get(pk = kwargs['pid'])
242 self.queryset = prj.get_all_compatible_machines()
243 self.queryset = self.queryset.order_by(self.default_orderby)
244
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500245 self.static_context_extra['current_layers'] = \
246 self.project_layers = \
247 prj.get_project_layer_versions(pk=True)
248
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500249 def setup_columns(self, *args, **kwargs):
250
251 self.add_column(title="Machine",
252 hideable=False,
253 orderable=True,
254 field_name="name")
255
256 self.add_column(title="Description",
257 field_name="description")
258
259 layer_link_template = '''
260 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
261 {{data.layer_version.layer.name}}</a>
262 '''
263
264 self.add_column(title="Layer",
265 static_data_name="layer_version__layer__name",
266 static_data_template=layer_link_template,
267 orderable=True)
268
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500269 self.add_column(title="Git revision",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500270 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",
271 hidden=True,
272 field_name="layer_version__get_vcs_reference")
273
274 machine_file_template = '''<code>conf/machine/{{data.name}}.conf</code>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600275 <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 -0500276
277 self.add_column(title="Machine file",
278 hidden=True,
279 static_data_name="machinefile",
280 static_data_template=machine_file_template)
281
282 self.add_column(title="Select",
283 help_text="Sets the selected machine as the project machine. You can only have one machine per project",
284 hideable=False,
285 filter_name="in_current_project",
286 static_data_name="add-del-layers",
287 static_data_template='{% include "machine_btn.html" %}')
288
289
290class LayerMachinesTable(MachinesTable):
291 """ Smaller version of the Machines table for use in layer details """
292
293 def __init__(self, *args, **kwargs):
294 super(LayerMachinesTable, self).__init__(*args, **kwargs)
295
296 def get_context_data(self, **kwargs):
297 context = super(LayerMachinesTable, self).get_context_data(**kwargs)
298 context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
299 return context
300
301
302 def setup_queryset(self, *args, **kwargs):
303 MachinesTable.setup_queryset(self, *args, **kwargs)
304
305 self.queryset = self.queryset.filter(layer_version__pk=int(kwargs['layerid']))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500306 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500307 self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
308
309 def setup_columns(self, *args, **kwargs):
310 self.add_column(title="Machine",
311 hideable=False,
312 orderable=True,
313 field_name="name")
314
315 self.add_column(title="Description",
316 field_name="description")
317
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600318 select_btn_template = '''
319 <a href="{% url "project" extra.pid %}?setMachine={{data.name}}"
320 class="btn btn-default btn-block select-machine-btn
321 {% if extra.in_prj == 0%}disabled{%endif%}">Select machine</a>
322 '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500323
324 self.add_column(title="Select machine",
325 static_data_name="add-del-layers",
326 static_data_template=select_btn_template)
327
328
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500329class RecipesTable(ToasterTable):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500330 """Table of All Recipes in Toaster"""
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500331
332 def __init__(self, *args, **kwargs):
333 super(RecipesTable, self).__init__(*args, **kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600334 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 -0500335
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500336 build_col = { 'title' : "Build",
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600337 'help_text' : "Before building a recipe, you might need to add the corresponding layer to your project",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500338 'hideable' : False,
339 'filter_name' : "in_current_project",
340 'static_data_name' : "add-del-layers",
341 'static_data_template' : '{% include "recipe_btn.html" %}'}
342
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500343 def get_context_data(self, **kwargs):
344 project = Project.objects.get(pk=kwargs['pid'])
345 context = super(RecipesTable, self).get_context_data(**kwargs)
346
347 context['project'] = project
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600348 context['projectlayers'] = [player.layercommit.id for player in ProjectLayer.objects.filter(project=context['project'])]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500349
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">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600398 <span class="glyphicon glyphicon-new-window"></i>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500399 </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 Williamsc0f7c042017-02-23 20:41:17 -0600431 revision_link_template = '''
432 {% if data.layer_version.layer.local_source_dir %}
433 <span class="text-muted">Not applicable</span>
434 <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>
435 {% else %}
436 {{data.layer_version.get_vcs_reference}}
437 {% endif %}
438 '''
439
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500440 self.add_column(title="Git revision",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500441 hidden=True,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600442 static_data_name="layer_version__get_vcs_reference",
443 static_data_template=revision_link_template)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500444
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500445
446class LayerRecipesTable(RecipesTable):
447 """ Smaller version of the Recipes table for use in layer details """
448
449 def __init__(self, *args, **kwargs):
450 super(LayerRecipesTable, self).__init__(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500451 self.default_orderby = "name"
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500452
453 def get_context_data(self, **kwargs):
454 context = super(LayerRecipesTable, self).get_context_data(**kwargs)
455 context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
456 return context
457
458
459 def setup_queryset(self, *args, **kwargs):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500460 self.queryset = \
461 Recipe.objects.filter(layer_version__pk=int(kwargs['layerid']))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500462
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500463 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500464 self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
465
466 def setup_columns(self, *args, **kwargs):
467 self.add_column(title="Recipe",
468 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",
469 hideable=False,
470 orderable=True,
471 field_name="name")
472
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500473 self.add_column(title="Version",
474 field_name="version")
475
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500476 self.add_column(title="Description",
477 field_name="get_description_or_summary")
478
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600479 build_recipe_template = '''
480 <a class="btn btn-default btn-block build-recipe-btn
481 {% if extra.in_prj == 0 %}disabled{% endif %}"
482 data-recipe-name="{{data.name}}">Build recipe</a>
483 '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500484
485 self.add_column(title="Build recipe",
486 static_data_name="add-del-layers",
487 static_data_template=build_recipe_template)
488
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500489class CustomImagesTable(ToasterTable):
490 """ Table to display your custom images """
491 def __init__(self, *args, **kwargs):
492 super(CustomImagesTable, self).__init__(*args, **kwargs)
493 self.title = "Custom images"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500494 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500495
496 def get_context_data(self, **kwargs):
497 context = super(CustomImagesTable, self).get_context_data(**kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600498
499 empty_state_template = '''
500 You have not created any custom images yet.
501 <a href="{% url 'newcustomimage' data.pid %}">
502 Create your first custom image</a>
503 '''
504 context['empty_state'] = self.render_static_data(empty_state_template,
505 kwargs)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500506 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600507
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500508 # TODO put project into the ToasterTable base class
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500509 context['project'] = project
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500510 return context
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500511
512 def setup_queryset(self, *args, **kwargs):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500513 prj = Project.objects.get(pk = kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500514 self.queryset = CustomImageRecipe.objects.filter(project=prj)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500515 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500516
517 def setup_columns(self, *args, **kwargs):
518
519 name_link_template = '''
520 <a href="{% url 'customrecipe' extra.pid data.id %}">
521 {{data.name}}
522 </a>
523 '''
524
525 self.add_column(title="Custom image",
526 hideable=False,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500527 orderable=True,
528 field_name="name",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500529 static_data_name="name",
530 static_data_template=name_link_template)
531
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500532 recipe_file_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600533 {% if data.get_base_recipe_file %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500534 <code>{{data.name}}_{{data.version}}.bb</code>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600535 <a href="{% url 'customrecipedownload' extra.pid data.pk %}"
536 class="glyphicon glyphicon-download-alt get-help" title="Download recipe file"></a>
537 {% endif %}'''
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500538
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500539 self.add_column(title="Recipe file",
540 static_data_name='recipe_file_download',
541 static_data_template=recipe_file_template)
542
543 approx_packages_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600544 {% if data.get_all_packages.count > 0 %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500545 <a href="{% url 'customrecipe' extra.pid data.id %}">
546 {{data.get_all_packages.count}}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600547 </a>
548 {% endif %}'''
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500549
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600550 self.add_column(title="Packages",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500551 static_data_name='approx_packages',
552 static_data_template=approx_packages_template)
553
554
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500555 build_btn_template = '''
556 <button data-recipe-name="{{data.name}}"
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600557 class="btn btn-default btn-block build-recipe-btn">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500558 Build
559 </button>'''
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500560
561 self.add_column(title="Build",
562 hideable=False,
563 static_data_name='build_custom_img',
564 static_data_template=build_btn_template)
565
566class ImageRecipesTable(RecipesTable):
567 """ A subset of the recipes table which displayed just image recipes """
568
569 def __init__(self, *args, **kwargs):
570 super(ImageRecipesTable, self).__init__(*args, **kwargs)
571 self.title = "Compatible image recipes"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500572 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500573
574 def setup_queryset(self, *args, **kwargs):
575 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
576
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500577 custom_image_recipes = CustomImageRecipe.objects.filter(
578 project=kwargs['pid'])
579 self.queryset = self.queryset.filter(
580 Q(is_image=True) & ~Q(pk__in=custom_image_recipes))
581 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500582
583
584 def setup_columns(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500585
586 name_link_template = '''
587 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
588 '''
589
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500590 self.add_column(title="Image recipe",
591 help_text="When you build an image recipe, you get an "
592 "image: a root file system you can"
593 "deploy to a machine",
594 hideable=False,
595 orderable=True,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500596 static_data_name="name",
597 static_data_template=name_link_template,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500598 field_name="name")
599
600 super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
601
602 self.add_column(**RecipesTable.build_col)
603
604
605class NewCustomImagesTable(ImageRecipesTable):
606 """ Table which displays Images recipes which can be customised """
607 def __init__(self, *args, **kwargs):
608 super(NewCustomImagesTable, self).__init__(*args, **kwargs)
609 self.title = "Select the image recipe you want to customise"
610
611 def setup_queryset(self, *args, **kwargs):
612 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500613 prj = Project.objects.get(pk = kwargs['pid'])
614 self.static_context_extra['current_layers'] = \
615 prj.get_project_layer_versions(pk=True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500616
617 self.queryset = self.queryset.filter(is_image=True)
618
619 def setup_columns(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500620
621 name_link_template = '''
622 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
623 '''
624
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500625 self.add_column(title="Image recipe",
626 help_text="When you build an image recipe, you get an "
627 "image: a root file system you can"
628 "deploy to a machine",
629 hideable=False,
630 orderable=True,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500631 static_data_name="name",
632 static_data_template=name_link_template,
633 field_name="name")
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500634
635 super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
636
637 self.add_column(title="Customise",
638 hideable=False,
639 filter_name="in_current_project",
640 static_data_name="customise-or-add-recipe",
641 static_data_template='{% include "customise_btn.html" %}')
642
643
644class SoftwareRecipesTable(RecipesTable):
645 """ Displays just the software recipes """
646 def __init__(self, *args, **kwargs):
647 super(SoftwareRecipesTable, self).__init__(*args, **kwargs)
648 self.title = "Compatible software recipes"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500649 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500650
651 def setup_queryset(self, *args, **kwargs):
652 super(SoftwareRecipesTable, self).setup_queryset(*args, **kwargs)
653
654 self.queryset = self.queryset.filter(is_image=False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500655 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500656
657
658 def setup_columns(self, *args, **kwargs):
659 self.add_column(title="Software recipe",
660 help_text="Information about a single piece of "
661 "software, including where to download the source, "
662 "configuration options, how to compile the source "
663 "files and how to package the compiled output",
664 hideable=False,
665 orderable=True,
666 field_name="name")
667
668 super(SoftwareRecipesTable, self).setup_columns(*args, **kwargs)
669
670 self.add_column(**RecipesTable.build_col)
671
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500672class PackagesTable(ToasterTable):
673 """ Table to display the packages in a recipe from it's last successful
674 build"""
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500675
676 def __init__(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500677 super(PackagesTable, self).__init__(*args, **kwargs)
678 self.title = "Packages included"
679 self.packages = None
680 self.default_orderby = "name"
681
682 def create_package_list(self, recipe, project_id):
683 """Creates a list of packages for the specified recipe by looking for
684 the last SUCCEEDED build of ther recipe"""
685
686 target = Target.objects.filter(Q(target=recipe.name) &
687 Q(build__project_id=project_id) &
688 Q(build__outcome=Build.SUCCEEDED)
689 ).last()
690
691 if target:
692 pkgs = target.target_installed_package_set.values_list('package',
693 flat=True)
694 return Package.objects.filter(pk__in=pkgs)
695
696 # Target/recipe never successfully built so empty queryset
697 return Package.objects.none()
698
699 def get_context_data(self, **kwargs):
700 """Context for rendering the sidebar and other items on the recipe
701 details page """
702 context = super(PackagesTable, self).get_context_data(**kwargs)
703
704 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
705 project = Project.objects.get(pk=kwargs['pid'])
706
707 in_project = (recipe.layer_version.pk in
708 project.get_project_layer_versions(pk=True))
709
710 packages = self.create_package_list(recipe, project.pk)
711
712 context.update({'project': project,
713 'recipe' : recipe,
714 'packages': packages,
715 'approx_pkg_size' : packages.aggregate(Sum('size')),
716 'in_project' : in_project,
717 })
718
719 return context
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500720
721 def setup_queryset(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500722 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600723 self.static_context_extra['target_name'] = recipe.name
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500724
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500725 self.queryset = self.create_package_list(recipe, kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500726 self.queryset = self.queryset.order_by('name')
727
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500728 def setup_columns(self, *args, **kwargs):
729 self.add_column(title="Package",
730 hideable=False,
731 orderable=True,
732 field_name="name")
733
734 self.add_column(title="Package Version",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500735 field_name="version",
736 hideable=False)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500737
738 self.add_column(title="Approx Size",
739 orderable=True,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600740 field_name="size",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500741 static_data_name="size",
742 static_data_template="{% load projecttags %} \
743 {{data.size|filtered_filesizeformat}}")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500744
745 self.add_column(title="License",
746 field_name="license",
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600747 orderable=True,
748 hidden=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500749
750
751 self.add_column(title="Dependencies",
752 static_data_name="dependencies",
753 static_data_template='\
754 {% include "snippets/pkg_dependencies_popover.html" %}')
755
756 self.add_column(title="Reverse dependencies",
757 static_data_name="reverse_dependencies",
758 static_data_template='\
759 {% include "snippets/pkg_revdependencies_popover.html" %}',
760 hidden=True)
761
762 self.add_column(title="Recipe",
763 field_name="recipe__name",
764 orderable=True,
765 hidden=True)
766
767 self.add_column(title="Recipe version",
768 field_name="recipe__version",
769 hidden=True)
770
771
772class SelectPackagesTable(PackagesTable):
773 """ Table to display the packages to add and remove from an image """
774
775 def __init__(self, *args, **kwargs):
776 super(SelectPackagesTable, self).__init__(*args, **kwargs)
777 self.title = "Add | Remove packages"
778
779 def setup_queryset(self, *args, **kwargs):
780 self.cust_recipe =\
781 CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
782 prj = Project.objects.get(pk = kwargs['pid'])
783
784 current_packages = self.cust_recipe.get_all_packages()
785
786 current_recipes = prj.get_available_recipes()
787
788 # only show packages where recipes->layers are in the project
789 self.queryset = CustomImagePackage.objects.filter(
790 ~Q(recipe=None) &
791 Q(recipe__in=current_recipes))
792
793 self.queryset = self.queryset.order_by('name')
794
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600795 # This target is the target used to work out which group of dependences
796 # to display, if we've built the custom image we use it otherwise we
797 # can use the based recipe instead
798 if prj.build_set.filter(target__target=self.cust_recipe.name).count()\
799 > 0:
800 self.static_context_extra['target_name'] = self.cust_recipe.name
801 else:
802 self.static_context_extra['target_name'] =\
803 Package_DependencyManager.TARGET_LATEST
804
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500805 self.static_context_extra['recipe_id'] = kwargs['custrecipeid']
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600806
807
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500808 self.static_context_extra['current_packages'] = \
809 current_packages.values_list('pk', flat=True)
810
811 def get_context_data(self, **kwargs):
812 # to reuse the Super class map the custrecipeid to the recipe_id
813 kwargs['recipe_id'] = kwargs['custrecipeid']
814 context = super(SelectPackagesTable, self).get_context_data(**kwargs)
815 custom_recipe = \
816 CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
817
818 context['recipe'] = custom_recipe
819 context['approx_pkg_size'] = \
820 custom_recipe.get_all_packages().aggregate(Sum('size'))
821 return context
822
823
824 def setup_columns(self, *args, **kwargs):
825 super(SelectPackagesTable, self).setup_columns(*args, **kwargs)
826
827 add_remove_template = '{% include "pkg_add_rm_btn.html" %}'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500828
829 self.add_column(title="Add | Remove",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500830 hideable=False,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500831 help_text="Use the add and remove buttons to modify "
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500832 "the package content of your custom image",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500833 static_data_name="add_rm_pkg_btn",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500834 static_data_template=add_remove_template,
835 filter_name='in_current_image_filter')
836
837 def setup_filters(self, *args, **kwargs):
838 in_current_image_filter = TableFilter(
839 'in_current_image_filter',
840 'Filter by added packages'
841 )
842
843 in_image_action = TableFilterActionToggle(
844 'in_image',
845 'Packages in %s' % self.cust_recipe.name,
846 Q(pk__in=self.static_context_extra['current_packages'])
847 )
848
849 not_in_image_action = TableFilterActionToggle(
850 'not_in_image',
851 'Packages not added to %s' % self.cust_recipe.name,
852 ~Q(pk__in=self.static_context_extra['current_packages'])
853 )
854
855 in_current_image_filter.add_action(in_image_action)
856 in_current_image_filter.add_action(not_in_image_action)
857 self.add_filter(in_current_image_filter)
858
859class ProjectsTable(ToasterTable):
860 """Table of projects in Toaster"""
861
862 def __init__(self, *args, **kwargs):
863 super(ProjectsTable, self).__init__(*args, **kwargs)
864 self.default_orderby = '-updated'
865 self.title = 'All projects'
866 self.static_context_extra['Build'] = Build
867
868 def get_context_data(self, **kwargs):
869 return super(ProjectsTable, self).get_context_data(**kwargs)
870
871 def setup_queryset(self, *args, **kwargs):
872 queryset = Project.objects.all()
873
874 # annotate each project with its number of builds
875 queryset = queryset.annotate(num_builds=Count('build'))
876
877 # exclude the command line builds project if it has no builds
878 q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0)
879 queryset = queryset.filter(Q(is_default=False) |
880 q_default_with_builds)
881
882 # order rows
883 queryset = queryset.order_by(self.default_orderby)
884
885 self.queryset = queryset
886
887 # columns: last activity on (updated) - DEFAULT, project (name), release,
888 # machine, number of builds, last build outcome, recipe (name), errors,
889 # warnings, image files
890 def setup_columns(self, *args, **kwargs):
891 name_template = '''
892 {% load project_url_tag %}
893 <span data-project-field="name">
894 <a href="{% project_url data %}">
895 {{data.name}}
896 </a>
897 </span>
898 '''
899
900 last_activity_on_template = '''
901 {% load project_url_tag %}
902 <span data-project-field="updated">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500903 {{data.updated | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500904 </span>
905 '''
906
907 release_template = '''
908 <span data-project-field="release">
909 {% if data.release %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600910 {{data.release.name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500911 {% elif data.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600912 <span class="text-muted">Not applicable</span>
913 <span class="glyphicon glyphicon-question-sign get-help hover-help"
914 title="This project does not have a release set.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500915 It simply collects information about the builds you start from
916 the command line while Toaster is running"
917 style="visibility: hidden;">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600918 </span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500919 {% else %}
920 No release available
921 {% endif %}
922 </span>
923 '''
924
925 machine_template = '''
926 <span data-project-field="machine">
927 {% if data.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600928 <span class="text-muted">Not applicable</span>
929 <span class="glyphicon glyphicon-question-sign get-help hover-help"
930 title="This project does not have a machine
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500931 set. It simply collects information about the builds you
932 start from the command line while Toaster is running"
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600933 style="visibility: hidden;"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500934 {% else %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600935 {{data.get_current_machine_name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500936 {% endif %}
937 </span>
938 '''
939
940 number_of_builds_template = '''
941 {% if data.get_number_of_builds > 0 %}
942 <a href="{% url 'projectbuilds' data.id %}">
943 {{data.get_number_of_builds}}
944 </a>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500945 {% endif %}
946 '''
947
948 last_build_outcome_template = '''
949 {% if data.get_number_of_builds > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600950 {% if data.get_last_outcome == extra.Build.SUCCEEDED %}
951 <span class="glyphicon glyphicon-ok-circle"></span>
952 {% elif data.get_last_outcome == extra.Build.FAILED %}
953 <span class="glyphicon glyphicon-minus-sign"></span>
954 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500955 {% endif %}
956 '''
957
958 recipe_template = '''
959 {% if data.get_number_of_builds > 0 %}
960 <a href="{% url "builddashboard" data.get_last_build_id %}">
961 {{data.get_last_target}}
962 </a>
963 {% endif %}
964 '''
965
966 errors_template = '''
967 {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600968 <a class="errors.count text-danger"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500969 href="{% url "builddashboard" data.get_last_build_id %}#errors">
970 {{data.get_last_errors}} error{{data.get_last_errors | pluralize}}
971 </a>
972 {% endif %}
973 '''
974
975 warnings_template = '''
976 {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600977 <a class="warnings.count text-warning"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500978 href="{% url "builddashboard" data.get_last_build_id %}#warnings">
979 {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}}
980 </a>
981 {% endif %}
982 '''
983
984 image_files_template = '''
985 {% if data.get_number_of_builds > 0 and data.get_last_outcome == extra.Build.SUCCEEDED %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600986 {{data.get_last_build_extensions}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500987 {% endif %}
988 '''
989
990 self.add_column(title='Project',
991 hideable=False,
992 orderable=True,
993 static_data_name='name',
994 static_data_template=name_template)
995
996 self.add_column(title='Last activity on',
997 help_text='Starting date and time of the \
998 last project build. If the project has no \
999 builds, this shows the date the project was \
1000 created.',
1001 hideable=False,
1002 orderable=True,
1003 static_data_name='updated',
1004 static_data_template=last_activity_on_template)
1005
1006 self.add_column(title='Release',
1007 help_text='The version of the build system used by \
1008 the project',
1009 hideable=False,
1010 orderable=True,
1011 static_data_name='release',
1012 static_data_template=release_template)
1013
1014 self.add_column(title='Machine',
1015 help_text='The hardware currently selected for the \
1016 project',
1017 hideable=False,
1018 orderable=False,
1019 static_data_name='machine',
1020 static_data_template=machine_template)
1021
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001022 self.add_column(title='Builds',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001023 help_text='The number of builds which have been run \
1024 for the project',
1025 hideable=False,
1026 orderable=False,
1027 static_data_name='number_of_builds',
1028 static_data_template=number_of_builds_template)
1029
1030 self.add_column(title='Last build outcome',
1031 help_text='Indicates whether the last project build \
1032 completed successfully or failed',
1033 hideable=True,
1034 orderable=False,
1035 static_data_name='last_build_outcome',
1036 static_data_template=last_build_outcome_template)
1037
1038 self.add_column(title='Recipe',
1039 help_text='The last recipe which was built in this \
1040 project',
1041 hideable=True,
1042 orderable=False,
1043 static_data_name='recipe_name',
1044 static_data_template=recipe_template)
1045
1046 self.add_column(title='Errors',
1047 help_text='The number of errors encountered during \
1048 the last project build (if any)',
1049 hideable=True,
1050 orderable=False,
1051 static_data_name='errors',
1052 static_data_template=errors_template)
1053
1054 self.add_column(title='Warnings',
1055 help_text='The number of warnings encountered during \
1056 the last project build (if any)',
1057 hideable=True,
1058 hidden=True,
1059 orderable=False,
1060 static_data_name='warnings',
1061 static_data_template=warnings_template)
1062
1063 self.add_column(title='Image files',
1064 help_text='The root file system types produced by \
1065 the last project build',
1066 hideable=True,
1067 hidden=True,
1068 orderable=False,
1069 static_data_name='image_files',
1070 static_data_template=image_files_template)
1071
1072class BuildsTable(ToasterTable):
1073 """Table of builds in Toaster"""
1074
1075 def __init__(self, *args, **kwargs):
1076 super(BuildsTable, self).__init__(*args, **kwargs)
1077 self.default_orderby = '-completed_on'
1078 self.static_context_extra['Build'] = Build
1079 self.static_context_extra['Task'] = Task
1080
1081 # attributes that are overridden in subclasses
1082
1083 # title for the page
1084 self.title = ''
1085
1086 # 'project' or 'all'; determines how the mrb (most recent builds)
1087 # section is displayed
1088 self.mrb_type = ''
1089
1090 def get_builds(self):
1091 """
1092 overridden in ProjectBuildsTable to return builds for a
1093 single project
1094 """
1095 return Build.objects.all()
1096
1097 def get_context_data(self, **kwargs):
1098 context = super(BuildsTable, self).get_context_data(**kwargs)
1099
1100 # should be set in subclasses
1101 context['mru'] = []
1102
1103 context['mrb_type'] = self.mrb_type
1104
1105 return context
1106
1107 def setup_queryset(self, *args, **kwargs):
1108 """
1109 The queryset is annotated so that it can be sorted by number of
1110 errors and number of warnings; but note that the criteria for
1111 finding the log messages to populate these fields should match those
1112 used in the Build model (orm/models.py) to populate the errors and
1113 warnings properties
1114 """
1115 queryset = self.get_builds()
1116
1117 # Don't include in progress builds pr cancelled builds
1118 queryset = queryset.exclude(Q(outcome=Build.IN_PROGRESS) |
1119 Q(outcome=Build.CANCELLED))
1120
1121 # sort
1122 queryset = queryset.order_by(self.default_orderby)
1123
1124 # annotate with number of ERROR, EXCEPTION and CRITICAL log messages
1125 criteria = (Q(logmessage__level=LogMessage.ERROR) |
1126 Q(logmessage__level=LogMessage.EXCEPTION) |
1127 Q(logmessage__level=LogMessage.CRITICAL))
1128
1129 queryset = queryset.annotate(
1130 errors_no=Count(
1131 Case(
1132 When(criteria, then=Value(1)),
1133 output_field=IntegerField()
1134 )
1135 )
1136 )
1137
1138 # annotate with number of WARNING log messages
1139 queryset = queryset.annotate(
1140 warnings_no=Count(
1141 Case(
1142 When(logmessage__level=LogMessage.WARNING, then=Value(1)),
1143 output_field=IntegerField()
1144 )
1145 )
1146 )
1147
1148 self.queryset = queryset
1149
1150 def setup_columns(self, *args, **kwargs):
1151 outcome_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001152 {% if data.outcome == data.SUCCEEDED %}
1153 <span class="glyphicon glyphicon-ok-circle"></span>
1154 {% elif data.outcome == data.FAILED %}
1155 <span class="glyphicon glyphicon-minus-sign"></span>
1156 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001157
1158 {% if data.cooker_log_path %}
1159 &nbsp;
1160 <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001161 <span class="glyphicon glyphicon-download-alt get-help"
1162 data-original-title="Download build log"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001163 </a>
1164 {% endif %}
1165 '''
1166
1167 recipe_template = '''
1168 {% for target_label in data.target_labels %}
1169 <a href="{% url "builddashboard" data.id %}">
1170 {{target_label}}
1171 </a>
1172 <br />
1173 {% endfor %}
1174 '''
1175
1176 machine_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001177 {{data.machine}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001178 '''
1179
1180 started_on_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001181 {{data.started_on | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001182 '''
1183
1184 completed_on_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001185 {{data.completed_on | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001186 '''
1187
1188 failed_tasks_template = '''
1189 {% if data.failed_tasks.count == 1 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001190 <a class="text-danger" href="{% url "task" data.id data.failed_tasks.0.id %}">
1191 <span>
1192 {{data.failed_tasks.0.recipe.name}} {{data.failed_tasks.0.task_name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001193 </span>
1194 </a>
1195 <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001196 <span class="glyphicon glyphicon-download-alt get-help"
1197 title="Download task log file">
1198 </span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001199 </a>
1200 {% elif data.failed_tasks.count > 1 %}
1201 <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001202 <span class="text-danger">{{data.failed_tasks.count}} tasks</span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001203 </a>
1204 {% endif %}
1205 '''
1206
1207 errors_template = '''
1208 {% if data.errors_no %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001209 <a class="errors.count text-danger" href="{% url "builddashboard" data.id %}#errors">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001210 {{data.errors_no}} error{{data.errors_no|pluralize}}
1211 </a>
1212 {% endif %}
1213 '''
1214
1215 warnings_template = '''
1216 {% if data.warnings_no %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001217 <a class="warnings.count text-warning" href="{% url "builddashboard" data.id %}#warnings">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001218 {{data.warnings_no}} warning{{data.warnings_no|pluralize}}
1219 </a>
1220 {% endif %}
1221 '''
1222
1223 time_template = '''
1224 {% load projecttags %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001225 {% if data.outcome == extra.Build.SUCCEEDED %}
1226 <a href="{% url "buildtime" data.id %}">
1227 {{data.timespent_seconds | sectohms}}
1228 </a>
1229 {% else %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001230 {{data.timespent_seconds | sectohms}}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001231 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001232 '''
1233
1234 image_files_template = '''
1235 {% if data.outcome == extra.Build.SUCCEEDED %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001236 {{data.get_image_file_extensions}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001237 {% endif %}
1238 '''
1239
1240 self.add_column(title='Outcome',
1241 help_text='Final state of the build (successful \
1242 or failed)',
1243 hideable=False,
1244 orderable=True,
1245 filter_name='outcome_filter',
1246 static_data_name='outcome',
1247 static_data_template=outcome_template)
1248
1249 self.add_column(title='Recipe',
1250 help_text='What was built (i.e. one or more recipes \
1251 or image recipes)',
1252 hideable=False,
1253 orderable=False,
1254 static_data_name='target',
1255 static_data_template=recipe_template)
1256
1257 self.add_column(title='Machine',
1258 help_text='Hardware for which you are building a \
1259 recipe or image recipe',
1260 hideable=False,
1261 orderable=True,
1262 static_data_name='machine',
1263 static_data_template=machine_template)
1264
1265 self.add_column(title='Started on',
1266 help_text='The date and time when the build started',
1267 hideable=True,
1268 hidden=True,
1269 orderable=True,
1270 filter_name='started_on_filter',
1271 static_data_name='started_on',
1272 static_data_template=started_on_template)
1273
1274 self.add_column(title='Completed on',
1275 help_text='The date and time when the build finished',
1276 hideable=False,
1277 orderable=True,
1278 filter_name='completed_on_filter',
1279 static_data_name='completed_on',
1280 static_data_template=completed_on_template)
1281
1282 self.add_column(title='Failed tasks',
1283 help_text='The number of tasks which failed during \
1284 the build',
1285 hideable=True,
1286 orderable=False,
1287 filter_name='failed_tasks_filter',
1288 static_data_name='failed_tasks',
1289 static_data_template=failed_tasks_template)
1290
1291 self.add_column(title='Errors',
1292 help_text='The number of errors encountered during \
1293 the build (if any)',
1294 hideable=True,
1295 orderable=True,
1296 static_data_name='errors_no',
1297 static_data_template=errors_template)
1298
1299 self.add_column(title='Warnings',
1300 help_text='The number of warnings encountered during \
1301 the build (if any)',
1302 hideable=True,
1303 orderable=True,
1304 static_data_name='warnings_no',
1305 static_data_template=warnings_template)
1306
1307 self.add_column(title='Time',
1308 help_text='How long the build took to finish',
1309 hideable=True,
1310 hidden=True,
1311 orderable=False,
1312 static_data_name='time',
1313 static_data_template=time_template)
1314
1315 self.add_column(title='Image files',
1316 help_text='The root file system types produced by \
1317 the build',
1318 hideable=True,
1319 orderable=False,
1320 static_data_name='image_files',
1321 static_data_template=image_files_template)
1322
1323 def setup_filters(self, *args, **kwargs):
1324 # outcomes
1325 outcome_filter = TableFilter(
1326 'outcome_filter',
1327 'Filter builds by outcome'
1328 )
1329
1330 successful_builds_action = TableFilterActionToggle(
1331 'successful_builds',
1332 'Successful builds',
1333 Q(outcome=Build.SUCCEEDED)
1334 )
1335
1336 failed_builds_action = TableFilterActionToggle(
1337 'failed_builds',
1338 'Failed builds',
1339 Q(outcome=Build.FAILED)
1340 )
1341
1342 outcome_filter.add_action(successful_builds_action)
1343 outcome_filter.add_action(failed_builds_action)
1344 self.add_filter(outcome_filter)
1345
1346 # started on
1347 started_on_filter = TableFilter(
1348 'started_on_filter',
1349 'Filter by date when build was started'
1350 )
1351
1352 started_today_action = TableFilterActionDay(
1353 'today',
1354 'Today\'s builds',
1355 'started_on',
1356 'today'
1357 )
1358
1359 started_yesterday_action = TableFilterActionDay(
1360 'yesterday',
1361 'Yesterday\'s builds',
1362 'started_on',
1363 'yesterday'
1364 )
1365
1366 by_started_date_range_action = TableFilterActionDateRange(
1367 'date_range',
1368 'Build date range',
1369 'started_on'
1370 )
1371
1372 started_on_filter.add_action(started_today_action)
1373 started_on_filter.add_action(started_yesterday_action)
1374 started_on_filter.add_action(by_started_date_range_action)
1375 self.add_filter(started_on_filter)
1376
1377 # completed on
1378 completed_on_filter = TableFilter(
1379 'completed_on_filter',
1380 'Filter by date when build was completed'
1381 )
1382
1383 completed_today_action = TableFilterActionDay(
1384 'today',
1385 'Today\'s builds',
1386 'completed_on',
1387 'today'
1388 )
1389
1390 completed_yesterday_action = TableFilterActionDay(
1391 'yesterday',
1392 'Yesterday\'s builds',
1393 'completed_on',
1394 'yesterday'
1395 )
1396
1397 by_completed_date_range_action = TableFilterActionDateRange(
1398 'date_range',
1399 'Build date range',
1400 'completed_on'
1401 )
1402
1403 completed_on_filter.add_action(completed_today_action)
1404 completed_on_filter.add_action(completed_yesterday_action)
1405 completed_on_filter.add_action(by_completed_date_range_action)
1406 self.add_filter(completed_on_filter)
1407
1408 # failed tasks
1409 failed_tasks_filter = TableFilter(
1410 'failed_tasks_filter',
1411 'Filter builds by failed tasks'
1412 )
1413
1414 criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
1415
1416 with_failed_tasks_action = TableFilterActionToggle(
1417 'with_failed_tasks',
1418 'Builds with failed tasks',
1419 criteria
1420 )
1421
1422 without_failed_tasks_action = TableFilterActionToggle(
1423 'without_failed_tasks',
1424 'Builds without failed tasks',
1425 ~criteria
1426 )
1427
1428 failed_tasks_filter.add_action(with_failed_tasks_action)
1429 failed_tasks_filter.add_action(without_failed_tasks_action)
1430 self.add_filter(failed_tasks_filter)
1431
1432
1433class AllBuildsTable(BuildsTable):
1434 """ Builds page for all builds """
1435
1436 def __init__(self, *args, **kwargs):
1437 super(AllBuildsTable, self).__init__(*args, **kwargs)
1438 self.title = 'All builds'
1439 self.mrb_type = 'all'
1440
1441 def setup_columns(self, *args, **kwargs):
1442 """
1443 All builds page shows a column for the project
1444 """
1445
1446 super(AllBuildsTable, self).setup_columns(*args, **kwargs)
1447
1448 project_template = '''
1449 {% load project_url_tag %}
1450 <a href="{% project_url data.project %}">
1451 {{data.project.name}}
1452 </a>
1453 {% if data.project.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001454 <span class="glyphicon glyphicon-question-sign get-help hover-help" title=""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001455 data-original-title="This project shows information about
1456 the builds you start from the command line while Toaster is
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001457 running" style="visibility: hidden;"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001458 {% endif %}
1459 '''
1460
1461 self.add_column(title='Project',
1462 hideable=True,
1463 orderable=True,
1464 static_data_name='project',
1465 static_data_template=project_template)
1466
1467 def get_context_data(self, **kwargs):
1468 """ Get all builds for the recent builds area """
1469 context = super(AllBuildsTable, self).get_context_data(**kwargs)
1470 context['mru'] = Build.get_recent()
1471 return context
1472
1473class ProjectBuildsTable(BuildsTable):
1474 """
1475 Builds page for a single project; a BuildsTable, with the queryset
1476 filtered by project
1477 """
1478
1479 def __init__(self, *args, **kwargs):
1480 super(ProjectBuildsTable, self).__init__(*args, **kwargs)
1481 self.title = 'All project builds'
1482 self.mrb_type = 'project'
1483
1484 # set from the querystring
1485 self.project_id = None
1486
1487 def setup_columns(self, *args, **kwargs):
1488 """
1489 Project builds table doesn't show the machines column by default
1490 """
1491
1492 super(ProjectBuildsTable, self).setup_columns(*args, **kwargs)
1493
1494 # hide the machine column
1495 self.set_column_hidden('Machine', True)
1496
1497 # allow the machine column to be hidden by the user
1498 self.set_column_hideable('Machine', True)
1499
1500 def setup_queryset(self, *args, **kwargs):
1501 """
1502 NOTE: self.project_id must be set before calling super(),
1503 as it's used in setup_queryset()
1504 """
1505 self.project_id = kwargs['pid']
1506 super(ProjectBuildsTable, self).setup_queryset(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001507 project = Project.objects.get(pk=self.project_id)
1508 self.queryset = self.queryset.filter(project=project)
1509
1510 def get_context_data(self, **kwargs):
1511 """
1512 Get recent builds for this project, and the project itself
1513
1514 NOTE: self.project_id must be set before calling super(),
1515 as it's used in get_context_data()
1516 """
1517 self.project_id = kwargs['pid']
1518 context = super(ProjectBuildsTable, self).get_context_data(**kwargs)
1519
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001520 empty_state_template = '''
1521 This project has no builds.
1522 <a href="{% url 'projectimagerecipes' data.pid %}">
1523 Choose a recipe to build</a>
1524 '''
1525 context['empty_state'] = self.render_static_data(empty_state_template,
1526 kwargs)
1527
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001528 project = Project.objects.get(pk=self.project_id)
1529 context['mru'] = Build.get_recent(project)
1530 context['project'] = project
1531
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001532 self.setup_queryset(**kwargs)
1533 if self.queryset.count() == 0 and \
1534 project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0:
1535 context['build_in_progress_none_completed'] = True
1536 else:
1537 context['build_in_progress_none_completed'] = False
1538
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001539 return context
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001540
1541
1542class DistrosTable(ToasterTable):
1543 """Table of Distros in Toaster"""
1544
1545 def __init__(self, *args, **kwargs):
1546 super(DistrosTable, self).__init__(*args, **kwargs)
1547 self.empty_state = "Toaster has no distro information for this project. Sadly, distro information cannot be obtained from builds, so this page will remain empty."
1548 self.title = "Compatible Distros"
1549 self.default_orderby = "name"
1550
1551 def get_context_data(self, **kwargs):
1552 context = super(DistrosTable, self).get_context_data(**kwargs)
1553 context['project'] = Project.objects.get(pk=kwargs['pid'])
1554 return context
1555
1556 def setup_filters(self, *args, **kwargs):
1557 project = Project.objects.get(pk=kwargs['pid'])
1558
1559 in_current_project_filter = TableFilter(
1560 "in_current_project",
1561 "Filter by project Distros"
1562 )
1563
1564 in_project_action = TableFilterActionToggle(
1565 "in_project",
1566 "Distro provided by layers added to this project",
1567 ProjectFilters.in_project(self.project_layers)
1568 )
1569
1570 not_in_project_action = TableFilterActionToggle(
1571 "not_in_project",
1572 "Distros provided by layers not added to this project",
1573 ProjectFilters.not_in_project(self.project_layers)
1574 )
1575
1576 in_current_project_filter.add_action(in_project_action)
1577 in_current_project_filter.add_action(not_in_project_action)
1578 self.add_filter(in_current_project_filter)
1579
1580 def setup_queryset(self, *args, **kwargs):
1581 prj = Project.objects.get(pk = kwargs['pid'])
1582 self.queryset = prj.get_all_compatible_distros()
1583 self.queryset = self.queryset.order_by(self.default_orderby)
1584
1585 self.static_context_extra['current_layers'] = \
1586 self.project_layers = \
1587 prj.get_project_layer_versions(pk=True)
1588
1589 def setup_columns(self, *args, **kwargs):
1590
1591 self.add_column(title="Distro",
1592 hideable=False,
1593 orderable=True,
1594 field_name="name")
1595
1596 self.add_column(title="Description",
1597 field_name="description")
1598
1599 layer_link_template = '''
1600 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
1601 {{data.layer_version.layer.name}}</a>
1602 '''
1603
1604 self.add_column(title="Layer",
1605 static_data_name="layer_version__layer__name",
1606 static_data_template=layer_link_template,
1607 orderable=True)
1608
1609 self.add_column(title="Git revision",
1610 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",
1611 hidden=True,
1612 field_name="layer_version__get_vcs_reference")
1613
1614 wrtemplate_file_template = '''<code>conf/machine/{{data.name}}.conf</code>
1615 <a href="{{data.get_vcs_machine_file_link_url}}" target="_blank"><span class="glyphicon glyphicon-new-window"></i></a>'''
1616
1617 self.add_column(title="Distro file",
1618 hidden=True,
1619 static_data_name="templatefile",
1620 static_data_template=wrtemplate_file_template)
1621
1622
1623 self.add_column(title="Select",
1624 help_text="Sets the selected distro to the project",
1625 hideable=False,
1626 filter_name="in_current_project",
1627 static_data_name="add-del-layers",
1628 static_data_template='{% include "distro_btn.html" %}')
1629