blob: b3ea2227e0f28ddada63596cb84e7b24d826b661 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05002# BitBake Toaster Implementation
3#
4# Copyright (C) 2015 Intel Corporation
5#
Brad Bishopc342db32019-05-15 21:57:59 -04006# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsc124f4f2015-09-15 14:41:29 -05007#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05008
9from toastergui.widgets import ToasterTable
10from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050011from orm.models import CustomImageRecipe, Package, Target, Build, LogMessage, Task
Patrick Williamsc0f7c042017-02-23 20:41:17 -060012from orm.models import CustomImagePackage, Package_DependencyManager
Brad Bishopd7bf8c12018-02-25 22:55:05 -050013from orm.models import Distro
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050014from django.db.models import Q, Max, Sum, Count, When, Case, Value, IntegerField
Patrick Williamsc124f4f2015-09-15 14:41:29 -050015from django.conf.urls import url
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050016from django.core.urlresolvers import reverse, resolve
17from django.http import HttpResponse
Patrick Williamsc124f4f2015-09-15 14:41:29 -050018from django.views.generic import TemplateView
19
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050020from toastergui.tablefilter import TableFilter
21from toastergui.tablefilter import TableFilterActionToggle
22from toastergui.tablefilter import TableFilterActionDateRange
23from toastergui.tablefilter import TableFilterActionDay
Patrick Williamsc124f4f2015-09-15 14:41:29 -050024
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080025import os
26
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050027class ProjectFilters(object):
28 @staticmethod
29 def in_project(project_layers):
30 return Q(layer_version__in=project_layers)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050031
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050032 @staticmethod
33 def not_in_project(project_layers):
34 return ~(ProjectFilters.in_project(project_layers))
Patrick Williamsc124f4f2015-09-15 14:41:29 -050035
36class LayersTable(ToasterTable):
37 """Table of layers in Toaster"""
38
39 def __init__(self, *args, **kwargs):
40 super(LayersTable, self).__init__(*args, **kwargs)
41 self.default_orderby = "layer__name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -050042 self.title = "Compatible layers"
Patrick Williamsc124f4f2015-09-15 14:41:29 -050043
44 def get_context_data(self, **kwargs):
45 context = super(LayersTable, self).get_context_data(**kwargs)
46
47 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -050048 context['project'] = project
Patrick Williamsc124f4f2015-09-15 14:41:29 -050049
50 return context
51
Patrick Williamsc124f4f2015-09-15 14:41:29 -050052 def setup_filters(self, *args, **kwargs):
53 project = Project.objects.get(pk=kwargs['pid'])
54 self.project_layers = ProjectLayer.objects.filter(project=project)
55
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050056 in_current_project_filter = TableFilter(
57 "in_current_project",
58 "Filter by project layers"
59 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050060
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050061 criteria = Q(projectlayer__in=self.project_layers)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050062
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050063 in_project_action = TableFilterActionToggle(
64 "in_project",
65 "Layers added to this project",
66 criteria
67 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050068
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050069 not_in_project_action = TableFilterActionToggle(
70 "not_in_project",
71 "Layers not added to this project",
72 ~criteria
73 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050074
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050075 in_current_project_filter.add_action(in_project_action)
76 in_current_project_filter.add_action(not_in_project_action)
77 self.add_filter(in_current_project_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050078
79 def setup_queryset(self, *args, **kwargs):
80 prj = Project.objects.get(pk = kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -050081 compatible_layers = prj.get_all_compatible_layer_versions()
82
83 self.static_context_extra['current_layers'] = \
84 prj.get_project_layer_versions(pk=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050085
86 self.queryset = compatible_layers.order_by(self.default_orderby)
87
88 def setup_columns(self, *args, **kwargs):
89
90 layer_link_template = '''
91 <a href="{% url 'layerdetails' extra.pid data.id %}">
92 {{data.layer.name}}
93 </a>
94 '''
95
96 self.add_column(title="Layer",
97 hideable=False,
98 orderable=True,
99 static_data_name="layer__name",
100 static_data_template=layer_link_template)
101
102 self.add_column(title="Summary",
103 field_name="layer__summary")
104
105 git_url_template = '''
106 <a href="{% url 'layerdetails' extra.pid data.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600107 {% if data.layer.local_source_dir %}
108 <code>{{data.layer.local_source_dir}}</code>
109 {% else %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500110 <code>{{data.layer.vcs_url}}</code>
111 </a>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600112 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500113 {% if data.get_vcs_link_url %}
114 <a target="_blank" href="{{ data.get_vcs_link_url }}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600115 <span class="glyphicon glyphicon-new-window"></span>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500116 </a>
117 {% endif %}
118 '''
119
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600120 self.add_column(title="Layer source code location",
121 help_text="A Git repository or an absolute path to a directory",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500122 hidden=True,
123 static_data_name="layer__vcs_url",
124 static_data_template=git_url_template)
125
126 git_dir_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600127 {% if data.layer.local_source_dir %}
128 <span class="text-muted">Not applicable</span>
129 <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>
130 {% else %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500131 <a href="{% url 'layerdetails' extra.pid data.id %}">
132 <code>{{data.dirpath}}</code>
133 </a>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600134 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500135 {% if data.dirpath and data.get_vcs_dirpath_link_url %}
136 <a target="_blank" href="{{ data.get_vcs_dirpath_link_url }}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600137 <span class="glyphicon glyphicon-new-window"></span>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500138 </a>
139 {% endif %}'''
140
141 self.add_column(title="Subdirectory",
142 help_text="The layer directory within the Git repository",
143 hidden=True,
144 static_data_name="git_subdir",
145 static_data_template=git_dir_template)
146
147 revision_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600148 {% if data.layer.local_source_dir %}
149 <span class="text-muted">Not applicable</span>
150 <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 -0500151 {% else %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600152 {% with vcs_ref=data.get_vcs_reference %}
153 {% include 'snippets/gitrev_popover.html' %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500154 {% endwith %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600155 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500156 '''
157
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500158 self.add_column(title="Git revision",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500159 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",
160 static_data_name="revision",
161 static_data_template=revision_template)
162
163 deps_template = '''
164 {% with ods=data.dependencies.all%}
165 {% if ods.count %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600166 <a class="btn btn-default" title="<a href='{% url "layerdetails" extra.pid data.id %}'>{{data.layer.name}}</a> dependencies"
167 data-content="<ul class='list-unstyled'>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500168 {% for i in ods%}
169 <li><a href='{% url "layerdetails" extra.pid i.depends_on.pk %}'>{{i.depends_on.layer.name}}</a></li>
170 {% endfor %}
171 </ul>">
172 {{ods.count}}
173 </a>
174 {% endif %}
175 {% endwith %}
176 '''
177
178 self.add_column(title="Dependencies",
179 help_text="Other layers a layer depends upon",
180 static_data_name="dependencies",
181 static_data_template=deps_template)
182
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500183 self.add_column(title="Add | Remove",
184 help_text="Add or remove layers to / from your project",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500185 hideable=False,
186 filter_name="in_current_project",
187 static_data_name="add-del-layers",
188 static_data_template='{% include "layer_btn.html" %}')
189
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500190
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500191class MachinesTable(ToasterTable):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500192 """Table of Machines in Toaster"""
193
194 def __init__(self, *args, **kwargs):
195 super(MachinesTable, self).__init__(*args, **kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600196 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 -0500197 self.title = "Compatible machines"
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500198 self.default_orderby = "name"
199
200 def get_context_data(self, **kwargs):
201 context = super(MachinesTable, self).get_context_data(**kwargs)
202 context['project'] = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500203 return context
204
205 def setup_filters(self, *args, **kwargs):
206 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500207
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500208 in_current_project_filter = TableFilter(
209 "in_current_project",
210 "Filter by project machines"
211 )
212
213 in_project_action = TableFilterActionToggle(
214 "in_project",
215 "Machines provided by layers added to this project",
216 ProjectFilters.in_project(self.project_layers)
217 )
218
219 not_in_project_action = TableFilterActionToggle(
220 "not_in_project",
221 "Machines provided by layers not added to this project",
222 ProjectFilters.not_in_project(self.project_layers)
223 )
224
225 in_current_project_filter.add_action(in_project_action)
226 in_current_project_filter.add_action(not_in_project_action)
227 self.add_filter(in_current_project_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500228
229 def setup_queryset(self, *args, **kwargs):
230 prj = Project.objects.get(pk = kwargs['pid'])
231 self.queryset = prj.get_all_compatible_machines()
232 self.queryset = self.queryset.order_by(self.default_orderby)
233
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500234 self.static_context_extra['current_layers'] = \
235 self.project_layers = \
236 prj.get_project_layer_versions(pk=True)
237
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500238 def setup_columns(self, *args, **kwargs):
239
240 self.add_column(title="Machine",
241 hideable=False,
242 orderable=True,
243 field_name="name")
244
245 self.add_column(title="Description",
246 field_name="description")
247
248 layer_link_template = '''
249 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
250 {{data.layer_version.layer.name}}</a>
251 '''
252
253 self.add_column(title="Layer",
254 static_data_name="layer_version__layer__name",
255 static_data_template=layer_link_template,
256 orderable=True)
257
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500258 self.add_column(title="Git revision",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500259 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",
260 hidden=True,
261 field_name="layer_version__get_vcs_reference")
262
263 machine_file_template = '''<code>conf/machine/{{data.name}}.conf</code>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600264 <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 -0500265
266 self.add_column(title="Machine file",
267 hidden=True,
268 static_data_name="machinefile",
269 static_data_template=machine_file_template)
270
271 self.add_column(title="Select",
272 help_text="Sets the selected machine as the project machine. You can only have one machine per project",
273 hideable=False,
274 filter_name="in_current_project",
275 static_data_name="add-del-layers",
276 static_data_template='{% include "machine_btn.html" %}')
277
278
279class LayerMachinesTable(MachinesTable):
280 """ Smaller version of the Machines table for use in layer details """
281
282 def __init__(self, *args, **kwargs):
283 super(LayerMachinesTable, self).__init__(*args, **kwargs)
284
285 def get_context_data(self, **kwargs):
286 context = super(LayerMachinesTable, self).get_context_data(**kwargs)
287 context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
288 return context
289
290
291 def setup_queryset(self, *args, **kwargs):
292 MachinesTable.setup_queryset(self, *args, **kwargs)
293
294 self.queryset = self.queryset.filter(layer_version__pk=int(kwargs['layerid']))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500295 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500296 self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
297
298 def setup_columns(self, *args, **kwargs):
299 self.add_column(title="Machine",
300 hideable=False,
301 orderable=True,
302 field_name="name")
303
304 self.add_column(title="Description",
305 field_name="description")
306
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600307 select_btn_template = '''
308 <a href="{% url "project" extra.pid %}?setMachine={{data.name}}"
309 class="btn btn-default btn-block select-machine-btn
310 {% if extra.in_prj == 0%}disabled{%endif%}">Select machine</a>
311 '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500312
313 self.add_column(title="Select machine",
314 static_data_name="add-del-layers",
315 static_data_template=select_btn_template)
316
317
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500318class RecipesTable(ToasterTable):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500319 """Table of All Recipes in Toaster"""
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500320
321 def __init__(self, *args, **kwargs):
322 super(RecipesTable, self).__init__(*args, **kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600323 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 -0500324
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500325 build_col = { 'title' : "Build",
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600326 'help_text' : "Before building a recipe, you might need to add the corresponding layer to your project",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500327 'hideable' : False,
328 'filter_name' : "in_current_project",
329 'static_data_name' : "add-del-layers",
330 'static_data_template' : '{% include "recipe_btn.html" %}'}
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800331 if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
332 build_col['static_data_template'] = '{% include "recipe_add_btn.html" %}'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500333
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500334 def get_context_data(self, **kwargs):
335 project = Project.objects.get(pk=kwargs['pid'])
336 context = super(RecipesTable, self).get_context_data(**kwargs)
337
338 context['project'] = project
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600339 context['projectlayers'] = [player.layercommit.id for player in ProjectLayer.objects.filter(project=context['project'])]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500340
341 return context
342
343 def setup_filters(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500344 table_filter = TableFilter(
345 'in_current_project',
346 'Filter by project recipes'
347 )
348
349 in_project_action = TableFilterActionToggle(
350 'in_project',
351 'Recipes provided by layers added to this project',
352 ProjectFilters.in_project(self.project_layers)
353 )
354
355 not_in_project_action = TableFilterActionToggle(
356 'not_in_project',
357 'Recipes provided by layers not added to this project',
358 ProjectFilters.not_in_project(self.project_layers)
359 )
360
361 table_filter.add_action(in_project_action)
362 table_filter.add_action(not_in_project_action)
363 self.add_filter(table_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500364
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500365 def setup_queryset(self, *args, **kwargs):
366 prj = Project.objects.get(pk = kwargs['pid'])
367
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500368 # Project layers used by the filters
369 self.project_layers = prj.get_project_layer_versions(pk=True)
370
371 # Project layers used to switch the button states
372 self.static_context_extra['current_layers'] = self.project_layers
373
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500374 self.queryset = prj.get_all_compatible_recipes()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500375
376
377 def setup_columns(self, *args, **kwargs):
378
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500379 self.add_column(title="Version",
380 hidden=False,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500381 field_name="version")
382
383 self.add_column(title="Description",
384 field_name="get_description_or_summary")
385
386 recipe_file_template = '''
387 <code>{{data.file_path}}</code>
388 <a href="{{data.get_vcs_recipe_file_link_url}}" target="_blank">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600389 <span class="glyphicon glyphicon-new-window"></i>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500390 </a>
391 '''
392
393 self.add_column(title="Recipe file",
394 help_text="Path to the recipe .bb file",
395 hidden=True,
396 static_data_name="recipe-file",
397 static_data_template=recipe_file_template)
398
399 self.add_column(title="Section",
400 help_text="The section in which recipes should be categorized",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500401 hidden=True,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500402 orderable=True,
403 field_name="section")
404
405 layer_link_template = '''
406 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
407 {{data.layer_version.layer.name}}</a>
408 '''
409
410 self.add_column(title="Layer",
411 help_text="The name of the layer providing the recipe",
412 orderable=True,
413 static_data_name="layer_version__layer__name",
414 static_data_template=layer_link_template)
415
416 self.add_column(title="License",
417 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 -0500418 hidden=True,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500419 orderable=True,
420 field_name="license")
421
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600422 revision_link_template = '''
423 {% if data.layer_version.layer.local_source_dir %}
424 <span class="text-muted">Not applicable</span>
425 <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>
426 {% else %}
427 {{data.layer_version.get_vcs_reference}}
428 {% endif %}
429 '''
430
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500431 self.add_column(title="Git revision",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500432 hidden=True,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600433 static_data_name="layer_version__get_vcs_reference",
434 static_data_template=revision_link_template)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500435
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500436
437class LayerRecipesTable(RecipesTable):
438 """ Smaller version of the Recipes table for use in layer details """
439
440 def __init__(self, *args, **kwargs):
441 super(LayerRecipesTable, self).__init__(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500442 self.default_orderby = "name"
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500443
444 def get_context_data(self, **kwargs):
445 context = super(LayerRecipesTable, self).get_context_data(**kwargs)
446 context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
447 return context
448
449
450 def setup_queryset(self, *args, **kwargs):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500451 self.queryset = \
452 Recipe.objects.filter(layer_version__pk=int(kwargs['layerid']))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500453
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500454 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500455 self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
456
457 def setup_columns(self, *args, **kwargs):
458 self.add_column(title="Recipe",
459 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",
460 hideable=False,
461 orderable=True,
462 field_name="name")
463
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500464 self.add_column(title="Version",
465 field_name="version")
466
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500467 self.add_column(title="Description",
468 field_name="get_description_or_summary")
469
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600470 build_recipe_template = '''
471 <a class="btn btn-default btn-block build-recipe-btn
472 {% if extra.in_prj == 0 %}disabled{% endif %}"
473 data-recipe-name="{{data.name}}">Build recipe</a>
474 '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500475
476 self.add_column(title="Build recipe",
477 static_data_name="add-del-layers",
478 static_data_template=build_recipe_template)
479
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500480class CustomImagesTable(ToasterTable):
481 """ Table to display your custom images """
482 def __init__(self, *args, **kwargs):
483 super(CustomImagesTable, self).__init__(*args, **kwargs)
484 self.title = "Custom images"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500485 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500486
487 def get_context_data(self, **kwargs):
488 context = super(CustomImagesTable, self).get_context_data(**kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600489
490 empty_state_template = '''
491 You have not created any custom images yet.
492 <a href="{% url 'newcustomimage' data.pid %}">
493 Create your first custom image</a>
494 '''
495 context['empty_state'] = self.render_static_data(empty_state_template,
496 kwargs)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500497 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600498
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500499 # TODO put project into the ToasterTable base class
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500500 context['project'] = project
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500501 return context
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500502
503 def setup_queryset(self, *args, **kwargs):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500504 prj = Project.objects.get(pk = kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500505 self.queryset = CustomImageRecipe.objects.filter(project=prj)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500506 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500507
508 def setup_columns(self, *args, **kwargs):
509
510 name_link_template = '''
511 <a href="{% url 'customrecipe' extra.pid data.id %}">
512 {{data.name}}
513 </a>
514 '''
515
516 self.add_column(title="Custom image",
517 hideable=False,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500518 orderable=True,
519 field_name="name",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500520 static_data_name="name",
521 static_data_template=name_link_template)
522
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500523 recipe_file_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600524 {% if data.get_base_recipe_file %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500525 <code>{{data.name}}_{{data.version}}.bb</code>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600526 <a href="{% url 'customrecipedownload' extra.pid data.pk %}"
527 class="glyphicon glyphicon-download-alt get-help" title="Download recipe file"></a>
528 {% endif %}'''
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500529
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500530 self.add_column(title="Recipe file",
531 static_data_name='recipe_file_download',
532 static_data_template=recipe_file_template)
533
534 approx_packages_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600535 {% if data.get_all_packages.count > 0 %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500536 <a href="{% url 'customrecipe' extra.pid data.id %}">
537 {{data.get_all_packages.count}}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600538 </a>
539 {% endif %}'''
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500540
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600541 self.add_column(title="Packages",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500542 static_data_name='approx_packages',
543 static_data_template=approx_packages_template)
544
545
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500546 build_btn_template = '''
547 <button data-recipe-name="{{data.name}}"
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600548 class="btn btn-default btn-block build-recipe-btn">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500549 Build
550 </button>'''
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500551
552 self.add_column(title="Build",
553 hideable=False,
554 static_data_name='build_custom_img',
555 static_data_template=build_btn_template)
556
557class ImageRecipesTable(RecipesTable):
558 """ A subset of the recipes table which displayed just image recipes """
559
560 def __init__(self, *args, **kwargs):
561 super(ImageRecipesTable, self).__init__(*args, **kwargs)
562 self.title = "Compatible image recipes"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500563 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500564
565 def setup_queryset(self, *args, **kwargs):
566 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
567
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500568 custom_image_recipes = CustomImageRecipe.objects.filter(
569 project=kwargs['pid'])
570 self.queryset = self.queryset.filter(
571 Q(is_image=True) & ~Q(pk__in=custom_image_recipes))
572 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500573
574
575 def setup_columns(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500576
577 name_link_template = '''
578 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
579 '''
580
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500581 self.add_column(title="Image recipe",
582 help_text="When you build an image recipe, you get an "
583 "image: a root file system you can"
584 "deploy to a machine",
585 hideable=False,
586 orderable=True,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500587 static_data_name="name",
588 static_data_template=name_link_template,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500589 field_name="name")
590
591 super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
592
593 self.add_column(**RecipesTable.build_col)
594
595
596class NewCustomImagesTable(ImageRecipesTable):
597 """ Table which displays Images recipes which can be customised """
598 def __init__(self, *args, **kwargs):
599 super(NewCustomImagesTable, self).__init__(*args, **kwargs)
600 self.title = "Select the image recipe you want to customise"
601
602 def setup_queryset(self, *args, **kwargs):
603 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500604 prj = Project.objects.get(pk = kwargs['pid'])
605 self.static_context_extra['current_layers'] = \
606 prj.get_project_layer_versions(pk=True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500607
608 self.queryset = self.queryset.filter(is_image=True)
609
610 def setup_columns(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500611
612 name_link_template = '''
613 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
614 '''
615
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500616 self.add_column(title="Image recipe",
617 help_text="When you build an image recipe, you get an "
618 "image: a root file system you can"
619 "deploy to a machine",
620 hideable=False,
621 orderable=True,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500622 static_data_name="name",
623 static_data_template=name_link_template,
624 field_name="name")
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500625
626 super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
627
628 self.add_column(title="Customise",
629 hideable=False,
630 filter_name="in_current_project",
631 static_data_name="customise-or-add-recipe",
632 static_data_template='{% include "customise_btn.html" %}')
633
634
635class SoftwareRecipesTable(RecipesTable):
636 """ Displays just the software recipes """
637 def __init__(self, *args, **kwargs):
638 super(SoftwareRecipesTable, self).__init__(*args, **kwargs)
639 self.title = "Compatible software recipes"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500640 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500641
642 def setup_queryset(self, *args, **kwargs):
643 super(SoftwareRecipesTable, self).setup_queryset(*args, **kwargs)
644
645 self.queryset = self.queryset.filter(is_image=False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500646 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500647
648
649 def setup_columns(self, *args, **kwargs):
650 self.add_column(title="Software recipe",
651 help_text="Information about a single piece of "
652 "software, including where to download the source, "
653 "configuration options, how to compile the source "
654 "files and how to package the compiled output",
655 hideable=False,
656 orderable=True,
657 field_name="name")
658
659 super(SoftwareRecipesTable, self).setup_columns(*args, **kwargs)
660
661 self.add_column(**RecipesTable.build_col)
662
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500663class PackagesTable(ToasterTable):
664 """ Table to display the packages in a recipe from it's last successful
665 build"""
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500666
667 def __init__(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500668 super(PackagesTable, self).__init__(*args, **kwargs)
669 self.title = "Packages included"
670 self.packages = None
671 self.default_orderby = "name"
672
673 def create_package_list(self, recipe, project_id):
674 """Creates a list of packages for the specified recipe by looking for
675 the last SUCCEEDED build of ther recipe"""
676
677 target = Target.objects.filter(Q(target=recipe.name) &
678 Q(build__project_id=project_id) &
679 Q(build__outcome=Build.SUCCEEDED)
680 ).last()
681
682 if target:
683 pkgs = target.target_installed_package_set.values_list('package',
684 flat=True)
685 return Package.objects.filter(pk__in=pkgs)
686
687 # Target/recipe never successfully built so empty queryset
688 return Package.objects.none()
689
690 def get_context_data(self, **kwargs):
691 """Context for rendering the sidebar and other items on the recipe
692 details page """
693 context = super(PackagesTable, self).get_context_data(**kwargs)
694
695 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
696 project = Project.objects.get(pk=kwargs['pid'])
697
698 in_project = (recipe.layer_version.pk in
699 project.get_project_layer_versions(pk=True))
700
701 packages = self.create_package_list(recipe, project.pk)
702
703 context.update({'project': project,
704 'recipe' : recipe,
705 'packages': packages,
706 'approx_pkg_size' : packages.aggregate(Sum('size')),
707 'in_project' : in_project,
708 })
709
710 return context
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500711
712 def setup_queryset(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500713 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600714 self.static_context_extra['target_name'] = recipe.name
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500715
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500716 self.queryset = self.create_package_list(recipe, kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500717 self.queryset = self.queryset.order_by('name')
718
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500719 def setup_columns(self, *args, **kwargs):
720 self.add_column(title="Package",
721 hideable=False,
722 orderable=True,
723 field_name="name")
724
725 self.add_column(title="Package Version",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500726 field_name="version",
727 hideable=False)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500728
729 self.add_column(title="Approx Size",
730 orderable=True,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600731 field_name="size",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500732 static_data_name="size",
733 static_data_template="{% load projecttags %} \
734 {{data.size|filtered_filesizeformat}}")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500735
736 self.add_column(title="License",
737 field_name="license",
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600738 orderable=True,
739 hidden=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500740
741
742 self.add_column(title="Dependencies",
743 static_data_name="dependencies",
744 static_data_template='\
745 {% include "snippets/pkg_dependencies_popover.html" %}')
746
747 self.add_column(title="Reverse dependencies",
748 static_data_name="reverse_dependencies",
749 static_data_template='\
750 {% include "snippets/pkg_revdependencies_popover.html" %}',
751 hidden=True)
752
753 self.add_column(title="Recipe",
754 field_name="recipe__name",
755 orderable=True,
756 hidden=True)
757
758 self.add_column(title="Recipe version",
759 field_name="recipe__version",
760 hidden=True)
761
762
763class SelectPackagesTable(PackagesTable):
764 """ Table to display the packages to add and remove from an image """
765
766 def __init__(self, *args, **kwargs):
767 super(SelectPackagesTable, self).__init__(*args, **kwargs)
768 self.title = "Add | Remove packages"
769
770 def setup_queryset(self, *args, **kwargs):
771 self.cust_recipe =\
772 CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
773 prj = Project.objects.get(pk = kwargs['pid'])
774
775 current_packages = self.cust_recipe.get_all_packages()
776
777 current_recipes = prj.get_available_recipes()
778
779 # only show packages where recipes->layers are in the project
780 self.queryset = CustomImagePackage.objects.filter(
781 ~Q(recipe=None) &
782 Q(recipe__in=current_recipes))
783
784 self.queryset = self.queryset.order_by('name')
785
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600786 # This target is the target used to work out which group of dependences
787 # to display, if we've built the custom image we use it otherwise we
788 # can use the based recipe instead
789 if prj.build_set.filter(target__target=self.cust_recipe.name).count()\
790 > 0:
791 self.static_context_extra['target_name'] = self.cust_recipe.name
792 else:
793 self.static_context_extra['target_name'] =\
794 Package_DependencyManager.TARGET_LATEST
795
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500796 self.static_context_extra['recipe_id'] = kwargs['custrecipeid']
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600797
798
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500799 self.static_context_extra['current_packages'] = \
800 current_packages.values_list('pk', flat=True)
801
802 def get_context_data(self, **kwargs):
803 # to reuse the Super class map the custrecipeid to the recipe_id
804 kwargs['recipe_id'] = kwargs['custrecipeid']
805 context = super(SelectPackagesTable, self).get_context_data(**kwargs)
806 custom_recipe = \
807 CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
808
809 context['recipe'] = custom_recipe
810 context['approx_pkg_size'] = \
811 custom_recipe.get_all_packages().aggregate(Sum('size'))
812 return context
813
814
815 def setup_columns(self, *args, **kwargs):
816 super(SelectPackagesTable, self).setup_columns(*args, **kwargs)
817
818 add_remove_template = '{% include "pkg_add_rm_btn.html" %}'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500819
820 self.add_column(title="Add | Remove",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500821 hideable=False,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500822 help_text="Use the add and remove buttons to modify "
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500823 "the package content of your custom image",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500824 static_data_name="add_rm_pkg_btn",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500825 static_data_template=add_remove_template,
826 filter_name='in_current_image_filter')
827
828 def setup_filters(self, *args, **kwargs):
829 in_current_image_filter = TableFilter(
830 'in_current_image_filter',
831 'Filter by added packages'
832 )
833
834 in_image_action = TableFilterActionToggle(
835 'in_image',
836 'Packages in %s' % self.cust_recipe.name,
837 Q(pk__in=self.static_context_extra['current_packages'])
838 )
839
840 not_in_image_action = TableFilterActionToggle(
841 'not_in_image',
842 'Packages not added to %s' % self.cust_recipe.name,
843 ~Q(pk__in=self.static_context_extra['current_packages'])
844 )
845
846 in_current_image_filter.add_action(in_image_action)
847 in_current_image_filter.add_action(not_in_image_action)
848 self.add_filter(in_current_image_filter)
849
850class ProjectsTable(ToasterTable):
851 """Table of projects in Toaster"""
852
853 def __init__(self, *args, **kwargs):
854 super(ProjectsTable, self).__init__(*args, **kwargs)
855 self.default_orderby = '-updated'
856 self.title = 'All projects'
857 self.static_context_extra['Build'] = Build
858
859 def get_context_data(self, **kwargs):
860 return super(ProjectsTable, self).get_context_data(**kwargs)
861
862 def setup_queryset(self, *args, **kwargs):
863 queryset = Project.objects.all()
864
865 # annotate each project with its number of builds
866 queryset = queryset.annotate(num_builds=Count('build'))
867
868 # exclude the command line builds project if it has no builds
869 q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0)
870 queryset = queryset.filter(Q(is_default=False) |
871 q_default_with_builds)
872
873 # order rows
874 queryset = queryset.order_by(self.default_orderby)
875
876 self.queryset = queryset
877
878 # columns: last activity on (updated) - DEFAULT, project (name), release,
879 # machine, number of builds, last build outcome, recipe (name), errors,
880 # warnings, image files
881 def setup_columns(self, *args, **kwargs):
882 name_template = '''
883 {% load project_url_tag %}
884 <span data-project-field="name">
885 <a href="{% project_url data %}">
886 {{data.name}}
887 </a>
888 </span>
889 '''
890
891 last_activity_on_template = '''
892 {% load project_url_tag %}
893 <span data-project-field="updated">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500894 {{data.updated | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500895 </span>
896 '''
897
898 release_template = '''
899 <span data-project-field="release">
900 {% if data.release %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600901 {{data.release.name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500902 {% elif data.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600903 <span class="text-muted">Not applicable</span>
904 <span class="glyphicon glyphicon-question-sign get-help hover-help"
905 title="This project does not have a release set.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500906 It simply collects information about the builds you start from
907 the command line while Toaster is running"
908 style="visibility: hidden;">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600909 </span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500910 {% else %}
911 No release available
912 {% endif %}
913 </span>
914 '''
915
916 machine_template = '''
917 <span data-project-field="machine">
918 {% if data.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600919 <span class="text-muted">Not applicable</span>
920 <span class="glyphicon glyphicon-question-sign get-help hover-help"
921 title="This project does not have a machine
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500922 set. It simply collects information about the builds you
923 start from the command line while Toaster is running"
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600924 style="visibility: hidden;"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500925 {% else %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600926 {{data.get_current_machine_name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500927 {% endif %}
928 </span>
929 '''
930
931 number_of_builds_template = '''
932 {% if data.get_number_of_builds > 0 %}
933 <a href="{% url 'projectbuilds' data.id %}">
934 {{data.get_number_of_builds}}
935 </a>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500936 {% endif %}
937 '''
938
939 last_build_outcome_template = '''
940 {% if data.get_number_of_builds > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600941 {% if data.get_last_outcome == extra.Build.SUCCEEDED %}
942 <span class="glyphicon glyphicon-ok-circle"></span>
943 {% elif data.get_last_outcome == extra.Build.FAILED %}
944 <span class="glyphicon glyphicon-minus-sign"></span>
945 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500946 {% endif %}
947 '''
948
949 recipe_template = '''
950 {% if data.get_number_of_builds > 0 %}
951 <a href="{% url "builddashboard" data.get_last_build_id %}">
952 {{data.get_last_target}}
953 </a>
954 {% endif %}
955 '''
956
957 errors_template = '''
958 {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600959 <a class="errors.count text-danger"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500960 href="{% url "builddashboard" data.get_last_build_id %}#errors">
961 {{data.get_last_errors}} error{{data.get_last_errors | pluralize}}
962 </a>
963 {% endif %}
964 '''
965
966 warnings_template = '''
967 {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600968 <a class="warnings.count text-warning"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500969 href="{% url "builddashboard" data.get_last_build_id %}#warnings">
970 {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}}
971 </a>
972 {% endif %}
973 '''
974
975 image_files_template = '''
976 {% if data.get_number_of_builds > 0 and data.get_last_outcome == extra.Build.SUCCEEDED %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600977 {{data.get_last_build_extensions}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500978 {% endif %}
979 '''
980
981 self.add_column(title='Project',
982 hideable=False,
983 orderable=True,
984 static_data_name='name',
985 static_data_template=name_template)
986
987 self.add_column(title='Last activity on',
988 help_text='Starting date and time of the \
989 last project build. If the project has no \
990 builds, this shows the date the project was \
991 created.',
992 hideable=False,
993 orderable=True,
994 static_data_name='updated',
995 static_data_template=last_activity_on_template)
996
997 self.add_column(title='Release',
998 help_text='The version of the build system used by \
999 the project',
1000 hideable=False,
1001 orderable=True,
1002 static_data_name='release',
1003 static_data_template=release_template)
1004
1005 self.add_column(title='Machine',
1006 help_text='The hardware currently selected for the \
1007 project',
1008 hideable=False,
1009 orderable=False,
1010 static_data_name='machine',
1011 static_data_template=machine_template)
1012
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001013 self.add_column(title='Builds',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001014 help_text='The number of builds which have been run \
1015 for the project',
1016 hideable=False,
1017 orderable=False,
1018 static_data_name='number_of_builds',
1019 static_data_template=number_of_builds_template)
1020
1021 self.add_column(title='Last build outcome',
1022 help_text='Indicates whether the last project build \
1023 completed successfully or failed',
1024 hideable=True,
1025 orderable=False,
1026 static_data_name='last_build_outcome',
1027 static_data_template=last_build_outcome_template)
1028
1029 self.add_column(title='Recipe',
1030 help_text='The last recipe which was built in this \
1031 project',
1032 hideable=True,
1033 orderable=False,
1034 static_data_name='recipe_name',
1035 static_data_template=recipe_template)
1036
1037 self.add_column(title='Errors',
1038 help_text='The number of errors encountered during \
1039 the last project build (if any)',
1040 hideable=True,
1041 orderable=False,
1042 static_data_name='errors',
1043 static_data_template=errors_template)
1044
1045 self.add_column(title='Warnings',
1046 help_text='The number of warnings encountered during \
1047 the last project build (if any)',
1048 hideable=True,
1049 hidden=True,
1050 orderable=False,
1051 static_data_name='warnings',
1052 static_data_template=warnings_template)
1053
1054 self.add_column(title='Image files',
1055 help_text='The root file system types produced by \
1056 the last project build',
1057 hideable=True,
1058 hidden=True,
1059 orderable=False,
1060 static_data_name='image_files',
1061 static_data_template=image_files_template)
1062
1063class BuildsTable(ToasterTable):
1064 """Table of builds in Toaster"""
1065
1066 def __init__(self, *args, **kwargs):
1067 super(BuildsTable, self).__init__(*args, **kwargs)
1068 self.default_orderby = '-completed_on'
1069 self.static_context_extra['Build'] = Build
1070 self.static_context_extra['Task'] = Task
1071
1072 # attributes that are overridden in subclasses
1073
1074 # title for the page
1075 self.title = ''
1076
1077 # 'project' or 'all'; determines how the mrb (most recent builds)
1078 # section is displayed
1079 self.mrb_type = ''
1080
1081 def get_builds(self):
1082 """
1083 overridden in ProjectBuildsTable to return builds for a
1084 single project
1085 """
1086 return Build.objects.all()
1087
1088 def get_context_data(self, **kwargs):
1089 context = super(BuildsTable, self).get_context_data(**kwargs)
1090
1091 # should be set in subclasses
1092 context['mru'] = []
1093
1094 context['mrb_type'] = self.mrb_type
1095
1096 return context
1097
1098 def setup_queryset(self, *args, **kwargs):
1099 """
1100 The queryset is annotated so that it can be sorted by number of
1101 errors and number of warnings; but note that the criteria for
1102 finding the log messages to populate these fields should match those
1103 used in the Build model (orm/models.py) to populate the errors and
1104 warnings properties
1105 """
1106 queryset = self.get_builds()
1107
1108 # Don't include in progress builds pr cancelled builds
1109 queryset = queryset.exclude(Q(outcome=Build.IN_PROGRESS) |
1110 Q(outcome=Build.CANCELLED))
1111
1112 # sort
1113 queryset = queryset.order_by(self.default_orderby)
1114
1115 # annotate with number of ERROR, EXCEPTION and CRITICAL log messages
1116 criteria = (Q(logmessage__level=LogMessage.ERROR) |
1117 Q(logmessage__level=LogMessage.EXCEPTION) |
1118 Q(logmessage__level=LogMessage.CRITICAL))
1119
1120 queryset = queryset.annotate(
1121 errors_no=Count(
1122 Case(
1123 When(criteria, then=Value(1)),
1124 output_field=IntegerField()
1125 )
1126 )
1127 )
1128
1129 # annotate with number of WARNING log messages
1130 queryset = queryset.annotate(
1131 warnings_no=Count(
1132 Case(
1133 When(logmessage__level=LogMessage.WARNING, then=Value(1)),
1134 output_field=IntegerField()
1135 )
1136 )
1137 )
1138
1139 self.queryset = queryset
1140
1141 def setup_columns(self, *args, **kwargs):
1142 outcome_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001143 {% if data.outcome == data.SUCCEEDED %}
1144 <span class="glyphicon glyphicon-ok-circle"></span>
1145 {% elif data.outcome == data.FAILED %}
1146 <span class="glyphicon glyphicon-minus-sign"></span>
1147 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001148
1149 {% if data.cooker_log_path %}
1150 &nbsp;
1151 <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001152 <span class="glyphicon glyphicon-download-alt get-help"
1153 data-original-title="Download build log"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001154 </a>
1155 {% endif %}
1156 '''
1157
1158 recipe_template = '''
1159 {% for target_label in data.target_labels %}
1160 <a href="{% url "builddashboard" data.id %}">
1161 {{target_label}}
1162 </a>
1163 <br />
1164 {% endfor %}
1165 '''
1166
1167 machine_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001168 {{data.machine}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001169 '''
1170
1171 started_on_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001172 {{data.started_on | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001173 '''
1174
1175 completed_on_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001176 {{data.completed_on | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001177 '''
1178
1179 failed_tasks_template = '''
1180 {% if data.failed_tasks.count == 1 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001181 <a class="text-danger" href="{% url "task" data.id data.failed_tasks.0.id %}">
1182 <span>
1183 {{data.failed_tasks.0.recipe.name}} {{data.failed_tasks.0.task_name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001184 </span>
1185 </a>
1186 <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001187 <span class="glyphicon glyphicon-download-alt get-help"
1188 title="Download task log file">
1189 </span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001190 </a>
1191 {% elif data.failed_tasks.count > 1 %}
1192 <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001193 <span class="text-danger">{{data.failed_tasks.count}} tasks</span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001194 </a>
1195 {% endif %}
1196 '''
1197
1198 errors_template = '''
1199 {% if data.errors_no %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001200 <a class="errors.count text-danger" href="{% url "builddashboard" data.id %}#errors">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001201 {{data.errors_no}} error{{data.errors_no|pluralize}}
1202 </a>
1203 {% endif %}
1204 '''
1205
1206 warnings_template = '''
1207 {% if data.warnings_no %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001208 <a class="warnings.count text-warning" href="{% url "builddashboard" data.id %}#warnings">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001209 {{data.warnings_no}} warning{{data.warnings_no|pluralize}}
1210 </a>
1211 {% endif %}
1212 '''
1213
1214 time_template = '''
1215 {% load projecttags %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001216 {% if data.outcome == extra.Build.SUCCEEDED %}
1217 <a href="{% url "buildtime" data.id %}">
1218 {{data.timespent_seconds | sectohms}}
1219 </a>
1220 {% else %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001221 {{data.timespent_seconds | sectohms}}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001222 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001223 '''
1224
1225 image_files_template = '''
1226 {% if data.outcome == extra.Build.SUCCEEDED %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001227 {{data.get_image_file_extensions}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001228 {% endif %}
1229 '''
1230
1231 self.add_column(title='Outcome',
1232 help_text='Final state of the build (successful \
1233 or failed)',
1234 hideable=False,
1235 orderable=True,
1236 filter_name='outcome_filter',
1237 static_data_name='outcome',
1238 static_data_template=outcome_template)
1239
1240 self.add_column(title='Recipe',
1241 help_text='What was built (i.e. one or more recipes \
1242 or image recipes)',
1243 hideable=False,
1244 orderable=False,
1245 static_data_name='target',
1246 static_data_template=recipe_template)
1247
1248 self.add_column(title='Machine',
1249 help_text='Hardware for which you are building a \
1250 recipe or image recipe',
1251 hideable=False,
1252 orderable=True,
1253 static_data_name='machine',
1254 static_data_template=machine_template)
1255
1256 self.add_column(title='Started on',
1257 help_text='The date and time when the build started',
1258 hideable=True,
1259 hidden=True,
1260 orderable=True,
1261 filter_name='started_on_filter',
1262 static_data_name='started_on',
1263 static_data_template=started_on_template)
1264
1265 self.add_column(title='Completed on',
1266 help_text='The date and time when the build finished',
1267 hideable=False,
1268 orderable=True,
1269 filter_name='completed_on_filter',
1270 static_data_name='completed_on',
1271 static_data_template=completed_on_template)
1272
1273 self.add_column(title='Failed tasks',
1274 help_text='The number of tasks which failed during \
1275 the build',
1276 hideable=True,
1277 orderable=False,
1278 filter_name='failed_tasks_filter',
1279 static_data_name='failed_tasks',
1280 static_data_template=failed_tasks_template)
1281
1282 self.add_column(title='Errors',
1283 help_text='The number of errors encountered during \
1284 the build (if any)',
1285 hideable=True,
1286 orderable=True,
1287 static_data_name='errors_no',
1288 static_data_template=errors_template)
1289
1290 self.add_column(title='Warnings',
1291 help_text='The number of warnings encountered during \
1292 the build (if any)',
1293 hideable=True,
1294 orderable=True,
1295 static_data_name='warnings_no',
1296 static_data_template=warnings_template)
1297
1298 self.add_column(title='Time',
1299 help_text='How long the build took to finish',
1300 hideable=True,
1301 hidden=True,
1302 orderable=False,
1303 static_data_name='time',
1304 static_data_template=time_template)
1305
1306 self.add_column(title='Image files',
1307 help_text='The root file system types produced by \
1308 the build',
1309 hideable=True,
1310 orderable=False,
1311 static_data_name='image_files',
1312 static_data_template=image_files_template)
1313
1314 def setup_filters(self, *args, **kwargs):
1315 # outcomes
1316 outcome_filter = TableFilter(
1317 'outcome_filter',
1318 'Filter builds by outcome'
1319 )
1320
1321 successful_builds_action = TableFilterActionToggle(
1322 'successful_builds',
1323 'Successful builds',
1324 Q(outcome=Build.SUCCEEDED)
1325 )
1326
1327 failed_builds_action = TableFilterActionToggle(
1328 'failed_builds',
1329 'Failed builds',
1330 Q(outcome=Build.FAILED)
1331 )
1332
1333 outcome_filter.add_action(successful_builds_action)
1334 outcome_filter.add_action(failed_builds_action)
1335 self.add_filter(outcome_filter)
1336
1337 # started on
1338 started_on_filter = TableFilter(
1339 'started_on_filter',
1340 'Filter by date when build was started'
1341 )
1342
1343 started_today_action = TableFilterActionDay(
1344 'today',
1345 'Today\'s builds',
1346 'started_on',
1347 'today'
1348 )
1349
1350 started_yesterday_action = TableFilterActionDay(
1351 'yesterday',
1352 'Yesterday\'s builds',
1353 'started_on',
1354 'yesterday'
1355 )
1356
1357 by_started_date_range_action = TableFilterActionDateRange(
1358 'date_range',
1359 'Build date range',
1360 'started_on'
1361 )
1362
1363 started_on_filter.add_action(started_today_action)
1364 started_on_filter.add_action(started_yesterday_action)
1365 started_on_filter.add_action(by_started_date_range_action)
1366 self.add_filter(started_on_filter)
1367
1368 # completed on
1369 completed_on_filter = TableFilter(
1370 'completed_on_filter',
1371 'Filter by date when build was completed'
1372 )
1373
1374 completed_today_action = TableFilterActionDay(
1375 'today',
1376 'Today\'s builds',
1377 'completed_on',
1378 'today'
1379 )
1380
1381 completed_yesterday_action = TableFilterActionDay(
1382 'yesterday',
1383 'Yesterday\'s builds',
1384 'completed_on',
1385 'yesterday'
1386 )
1387
1388 by_completed_date_range_action = TableFilterActionDateRange(
1389 'date_range',
1390 'Build date range',
1391 'completed_on'
1392 )
1393
1394 completed_on_filter.add_action(completed_today_action)
1395 completed_on_filter.add_action(completed_yesterday_action)
1396 completed_on_filter.add_action(by_completed_date_range_action)
1397 self.add_filter(completed_on_filter)
1398
1399 # failed tasks
1400 failed_tasks_filter = TableFilter(
1401 'failed_tasks_filter',
1402 'Filter builds by failed tasks'
1403 )
1404
1405 criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
1406
1407 with_failed_tasks_action = TableFilterActionToggle(
1408 'with_failed_tasks',
1409 'Builds with failed tasks',
1410 criteria
1411 )
1412
1413 without_failed_tasks_action = TableFilterActionToggle(
1414 'without_failed_tasks',
1415 'Builds without failed tasks',
1416 ~criteria
1417 )
1418
1419 failed_tasks_filter.add_action(with_failed_tasks_action)
1420 failed_tasks_filter.add_action(without_failed_tasks_action)
1421 self.add_filter(failed_tasks_filter)
1422
1423
1424class AllBuildsTable(BuildsTable):
1425 """ Builds page for all builds """
1426
1427 def __init__(self, *args, **kwargs):
1428 super(AllBuildsTable, self).__init__(*args, **kwargs)
1429 self.title = 'All builds'
1430 self.mrb_type = 'all'
1431
1432 def setup_columns(self, *args, **kwargs):
1433 """
1434 All builds page shows a column for the project
1435 """
1436
1437 super(AllBuildsTable, self).setup_columns(*args, **kwargs)
1438
1439 project_template = '''
1440 {% load project_url_tag %}
1441 <a href="{% project_url data.project %}">
1442 {{data.project.name}}
1443 </a>
1444 {% if data.project.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001445 <span class="glyphicon glyphicon-question-sign get-help hover-help" title=""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001446 data-original-title="This project shows information about
1447 the builds you start from the command line while Toaster is
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001448 running" style="visibility: hidden;"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001449 {% endif %}
1450 '''
1451
1452 self.add_column(title='Project',
1453 hideable=True,
1454 orderable=True,
1455 static_data_name='project',
1456 static_data_template=project_template)
1457
1458 def get_context_data(self, **kwargs):
1459 """ Get all builds for the recent builds area """
1460 context = super(AllBuildsTable, self).get_context_data(**kwargs)
1461 context['mru'] = Build.get_recent()
1462 return context
1463
1464class ProjectBuildsTable(BuildsTable):
1465 """
1466 Builds page for a single project; a BuildsTable, with the queryset
1467 filtered by project
1468 """
1469
1470 def __init__(self, *args, **kwargs):
1471 super(ProjectBuildsTable, self).__init__(*args, **kwargs)
1472 self.title = 'All project builds'
1473 self.mrb_type = 'project'
1474
1475 # set from the querystring
1476 self.project_id = None
1477
1478 def setup_columns(self, *args, **kwargs):
1479 """
1480 Project builds table doesn't show the machines column by default
1481 """
1482
1483 super(ProjectBuildsTable, self).setup_columns(*args, **kwargs)
1484
1485 # hide the machine column
1486 self.set_column_hidden('Machine', True)
1487
1488 # allow the machine column to be hidden by the user
1489 self.set_column_hideable('Machine', True)
1490
1491 def setup_queryset(self, *args, **kwargs):
1492 """
1493 NOTE: self.project_id must be set before calling super(),
1494 as it's used in setup_queryset()
1495 """
1496 self.project_id = kwargs['pid']
1497 super(ProjectBuildsTable, self).setup_queryset(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001498 project = Project.objects.get(pk=self.project_id)
1499 self.queryset = self.queryset.filter(project=project)
1500
1501 def get_context_data(self, **kwargs):
1502 """
1503 Get recent builds for this project, and the project itself
1504
1505 NOTE: self.project_id must be set before calling super(),
1506 as it's used in get_context_data()
1507 """
1508 self.project_id = kwargs['pid']
1509 context = super(ProjectBuildsTable, self).get_context_data(**kwargs)
1510
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001511 empty_state_template = '''
1512 This project has no builds.
1513 <a href="{% url 'projectimagerecipes' data.pid %}">
1514 Choose a recipe to build</a>
1515 '''
1516 context['empty_state'] = self.render_static_data(empty_state_template,
1517 kwargs)
1518
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001519 project = Project.objects.get(pk=self.project_id)
1520 context['mru'] = Build.get_recent(project)
1521 context['project'] = project
1522
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001523 self.setup_queryset(**kwargs)
1524 if self.queryset.count() == 0 and \
1525 project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0:
1526 context['build_in_progress_none_completed'] = True
1527 else:
1528 context['build_in_progress_none_completed'] = False
1529
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001530 return context
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001531
1532
1533class DistrosTable(ToasterTable):
1534 """Table of Distros in Toaster"""
1535
1536 def __init__(self, *args, **kwargs):
1537 super(DistrosTable, self).__init__(*args, **kwargs)
1538 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."
1539 self.title = "Compatible Distros"
1540 self.default_orderby = "name"
1541
1542 def get_context_data(self, **kwargs):
1543 context = super(DistrosTable, self).get_context_data(**kwargs)
1544 context['project'] = Project.objects.get(pk=kwargs['pid'])
1545 return context
1546
1547 def setup_filters(self, *args, **kwargs):
1548 project = Project.objects.get(pk=kwargs['pid'])
1549
1550 in_current_project_filter = TableFilter(
1551 "in_current_project",
1552 "Filter by project Distros"
1553 )
1554
1555 in_project_action = TableFilterActionToggle(
1556 "in_project",
1557 "Distro provided by layers added to this project",
1558 ProjectFilters.in_project(self.project_layers)
1559 )
1560
1561 not_in_project_action = TableFilterActionToggle(
1562 "not_in_project",
1563 "Distros provided by layers not added to this project",
1564 ProjectFilters.not_in_project(self.project_layers)
1565 )
1566
1567 in_current_project_filter.add_action(in_project_action)
1568 in_current_project_filter.add_action(not_in_project_action)
1569 self.add_filter(in_current_project_filter)
1570
1571 def setup_queryset(self, *args, **kwargs):
1572 prj = Project.objects.get(pk = kwargs['pid'])
1573 self.queryset = prj.get_all_compatible_distros()
1574 self.queryset = self.queryset.order_by(self.default_orderby)
1575
1576 self.static_context_extra['current_layers'] = \
1577 self.project_layers = \
1578 prj.get_project_layer_versions(pk=True)
1579
1580 def setup_columns(self, *args, **kwargs):
1581
1582 self.add_column(title="Distro",
1583 hideable=False,
1584 orderable=True,
1585 field_name="name")
1586
1587 self.add_column(title="Description",
1588 field_name="description")
1589
1590 layer_link_template = '''
1591 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
1592 {{data.layer_version.layer.name}}</a>
1593 '''
1594
1595 self.add_column(title="Layer",
1596 static_data_name="layer_version__layer__name",
1597 static_data_template=layer_link_template,
1598 orderable=True)
1599
1600 self.add_column(title="Git revision",
1601 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",
1602 hidden=True,
1603 field_name="layer_version__get_vcs_reference")
1604
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001605 distro_file_template = '''<code>conf/distro/{{data.name}}.conf</code>
1606 {% if 'None' not in data.get_vcs_distro_file_link_url %}<a href="{{data.get_vcs_distro_file_link_url}}" target="_blank"><span class="glyphicon glyphicon-new-window"></i></a>{% endif %}'''
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001607 self.add_column(title="Distro file",
1608 hidden=True,
1609 static_data_name="templatefile",
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001610 static_data_template=distro_file_template)
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001611
1612 self.add_column(title="Select",
1613 help_text="Sets the selected distro to the project",
1614 hideable=False,
1615 filter_name="in_current_project",
1616 static_data_name="add-del-layers",
1617 static_data_template='{% include "distro_btn.html" %}')
1618