blob: 9ff756bc8dfbcf5bdc07ed1ba01741c0b3b64844 [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
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080038import os
39
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050040class ProjectFilters(object):
41 @staticmethod
42 def in_project(project_layers):
43 return Q(layer_version__in=project_layers)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050044
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050045 @staticmethod
46 def not_in_project(project_layers):
47 return ~(ProjectFilters.in_project(project_layers))
Patrick Williamsc124f4f2015-09-15 14:41:29 -050048
49class LayersTable(ToasterTable):
50 """Table of layers in Toaster"""
51
52 def __init__(self, *args, **kwargs):
53 super(LayersTable, self).__init__(*args, **kwargs)
54 self.default_orderby = "layer__name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -050055 self.title = "Compatible layers"
Patrick Williamsc124f4f2015-09-15 14:41:29 -050056
57 def get_context_data(self, **kwargs):
58 context = super(LayersTable, self).get_context_data(**kwargs)
59
60 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -050061 context['project'] = project
Patrick Williamsc124f4f2015-09-15 14:41:29 -050062
63 return context
64
Patrick Williamsc124f4f2015-09-15 14:41:29 -050065 def setup_filters(self, *args, **kwargs):
66 project = Project.objects.get(pk=kwargs['pid'])
67 self.project_layers = ProjectLayer.objects.filter(project=project)
68
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050069 in_current_project_filter = TableFilter(
70 "in_current_project",
71 "Filter by project layers"
72 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050073
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050074 criteria = Q(projectlayer__in=self.project_layers)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050075
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050076 in_project_action = TableFilterActionToggle(
77 "in_project",
78 "Layers added to this project",
79 criteria
80 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050081
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050082 not_in_project_action = TableFilterActionToggle(
83 "not_in_project",
84 "Layers not added to this project",
85 ~criteria
86 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -050087
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050088 in_current_project_filter.add_action(in_project_action)
89 in_current_project_filter.add_action(not_in_project_action)
90 self.add_filter(in_current_project_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050091
92 def setup_queryset(self, *args, **kwargs):
93 prj = Project.objects.get(pk = kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -050094 compatible_layers = prj.get_all_compatible_layer_versions()
95
96 self.static_context_extra['current_layers'] = \
97 prj.get_project_layer_versions(pk=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050098
99 self.queryset = compatible_layers.order_by(self.default_orderby)
100
101 def setup_columns(self, *args, **kwargs):
102
103 layer_link_template = '''
104 <a href="{% url 'layerdetails' extra.pid data.id %}">
105 {{data.layer.name}}
106 </a>
107 '''
108
109 self.add_column(title="Layer",
110 hideable=False,
111 orderable=True,
112 static_data_name="layer__name",
113 static_data_template=layer_link_template)
114
115 self.add_column(title="Summary",
116 field_name="layer__summary")
117
118 git_url_template = '''
119 <a href="{% url 'layerdetails' extra.pid data.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600120 {% if data.layer.local_source_dir %}
121 <code>{{data.layer.local_source_dir}}</code>
122 {% else %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500123 <code>{{data.layer.vcs_url}}</code>
124 </a>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600125 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500126 {% if data.get_vcs_link_url %}
127 <a target="_blank" href="{{ data.get_vcs_link_url }}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600128 <span class="glyphicon glyphicon-new-window"></span>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500129 </a>
130 {% endif %}
131 '''
132
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600133 self.add_column(title="Layer source code location",
134 help_text="A Git repository or an absolute path to a directory",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500135 hidden=True,
136 static_data_name="layer__vcs_url",
137 static_data_template=git_url_template)
138
139 git_dir_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600140 {% if data.layer.local_source_dir %}
141 <span class="text-muted">Not applicable</span>
142 <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>
143 {% else %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500144 <a href="{% url 'layerdetails' extra.pid data.id %}">
145 <code>{{data.dirpath}}</code>
146 </a>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600147 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500148 {% if data.dirpath and data.get_vcs_dirpath_link_url %}
149 <a target="_blank" href="{{ data.get_vcs_dirpath_link_url }}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600150 <span class="glyphicon glyphicon-new-window"></span>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500151 </a>
152 {% endif %}'''
153
154 self.add_column(title="Subdirectory",
155 help_text="The layer directory within the Git repository",
156 hidden=True,
157 static_data_name="git_subdir",
158 static_data_template=git_dir_template)
159
160 revision_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600161 {% if data.layer.local_source_dir %}
162 <span class="text-muted">Not applicable</span>
163 <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 -0500164 {% else %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600165 {% with vcs_ref=data.get_vcs_reference %}
166 {% include 'snippets/gitrev_popover.html' %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500167 {% endwith %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600168 {% endif %}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500169 '''
170
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500171 self.add_column(title="Git revision",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500172 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",
173 static_data_name="revision",
174 static_data_template=revision_template)
175
176 deps_template = '''
177 {% with ods=data.dependencies.all%}
178 {% if ods.count %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600179 <a class="btn btn-default" title="<a href='{% url "layerdetails" extra.pid data.id %}'>{{data.layer.name}}</a> dependencies"
180 data-content="<ul class='list-unstyled'>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500181 {% for i in ods%}
182 <li><a href='{% url "layerdetails" extra.pid i.depends_on.pk %}'>{{i.depends_on.layer.name}}</a></li>
183 {% endfor %}
184 </ul>">
185 {{ods.count}}
186 </a>
187 {% endif %}
188 {% endwith %}
189 '''
190
191 self.add_column(title="Dependencies",
192 help_text="Other layers a layer depends upon",
193 static_data_name="dependencies",
194 static_data_template=deps_template)
195
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500196 self.add_column(title="Add | Remove",
197 help_text="Add or remove layers to / from your project",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500198 hideable=False,
199 filter_name="in_current_project",
200 static_data_name="add-del-layers",
201 static_data_template='{% include "layer_btn.html" %}')
202
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500203
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500204class MachinesTable(ToasterTable):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500205 """Table of Machines in Toaster"""
206
207 def __init__(self, *args, **kwargs):
208 super(MachinesTable, self).__init__(*args, **kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600209 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 -0500210 self.title = "Compatible machines"
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500211 self.default_orderby = "name"
212
213 def get_context_data(self, **kwargs):
214 context = super(MachinesTable, self).get_context_data(**kwargs)
215 context['project'] = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500216 return context
217
218 def setup_filters(self, *args, **kwargs):
219 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500220
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500221 in_current_project_filter = TableFilter(
222 "in_current_project",
223 "Filter by project machines"
224 )
225
226 in_project_action = TableFilterActionToggle(
227 "in_project",
228 "Machines provided by layers added to this project",
229 ProjectFilters.in_project(self.project_layers)
230 )
231
232 not_in_project_action = TableFilterActionToggle(
233 "not_in_project",
234 "Machines provided by layers not added to this project",
235 ProjectFilters.not_in_project(self.project_layers)
236 )
237
238 in_current_project_filter.add_action(in_project_action)
239 in_current_project_filter.add_action(not_in_project_action)
240 self.add_filter(in_current_project_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500241
242 def setup_queryset(self, *args, **kwargs):
243 prj = Project.objects.get(pk = kwargs['pid'])
244 self.queryset = prj.get_all_compatible_machines()
245 self.queryset = self.queryset.order_by(self.default_orderby)
246
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500247 self.static_context_extra['current_layers'] = \
248 self.project_layers = \
249 prj.get_project_layer_versions(pk=True)
250
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500251 def setup_columns(self, *args, **kwargs):
252
253 self.add_column(title="Machine",
254 hideable=False,
255 orderable=True,
256 field_name="name")
257
258 self.add_column(title="Description",
259 field_name="description")
260
261 layer_link_template = '''
262 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
263 {{data.layer_version.layer.name}}</a>
264 '''
265
266 self.add_column(title="Layer",
267 static_data_name="layer_version__layer__name",
268 static_data_template=layer_link_template,
269 orderable=True)
270
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500271 self.add_column(title="Git revision",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500272 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",
273 hidden=True,
274 field_name="layer_version__get_vcs_reference")
275
276 machine_file_template = '''<code>conf/machine/{{data.name}}.conf</code>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600277 <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 -0500278
279 self.add_column(title="Machine file",
280 hidden=True,
281 static_data_name="machinefile",
282 static_data_template=machine_file_template)
283
284 self.add_column(title="Select",
285 help_text="Sets the selected machine as the project machine. You can only have one machine per project",
286 hideable=False,
287 filter_name="in_current_project",
288 static_data_name="add-del-layers",
289 static_data_template='{% include "machine_btn.html" %}')
290
291
292class LayerMachinesTable(MachinesTable):
293 """ Smaller version of the Machines table for use in layer details """
294
295 def __init__(self, *args, **kwargs):
296 super(LayerMachinesTable, self).__init__(*args, **kwargs)
297
298 def get_context_data(self, **kwargs):
299 context = super(LayerMachinesTable, self).get_context_data(**kwargs)
300 context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
301 return context
302
303
304 def setup_queryset(self, *args, **kwargs):
305 MachinesTable.setup_queryset(self, *args, **kwargs)
306
307 self.queryset = self.queryset.filter(layer_version__pk=int(kwargs['layerid']))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500308 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500309 self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
310
311 def setup_columns(self, *args, **kwargs):
312 self.add_column(title="Machine",
313 hideable=False,
314 orderable=True,
315 field_name="name")
316
317 self.add_column(title="Description",
318 field_name="description")
319
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600320 select_btn_template = '''
321 <a href="{% url "project" extra.pid %}?setMachine={{data.name}}"
322 class="btn btn-default btn-block select-machine-btn
323 {% if extra.in_prj == 0%}disabled{%endif%}">Select machine</a>
324 '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500325
326 self.add_column(title="Select machine",
327 static_data_name="add-del-layers",
328 static_data_template=select_btn_template)
329
330
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500331class RecipesTable(ToasterTable):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500332 """Table of All Recipes in Toaster"""
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500333
334 def __init__(self, *args, **kwargs):
335 super(RecipesTable, self).__init__(*args, **kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600336 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 -0500337
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500338 build_col = { 'title' : "Build",
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600339 'help_text' : "Before building a recipe, you might need to add the corresponding layer to your project",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500340 'hideable' : False,
341 'filter_name' : "in_current_project",
342 'static_data_name' : "add-del-layers",
343 'static_data_template' : '{% include "recipe_btn.html" %}'}
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800344 if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
345 build_col['static_data_template'] = '{% include "recipe_add_btn.html" %}'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500346
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500347 def get_context_data(self, **kwargs):
348 project = Project.objects.get(pk=kwargs['pid'])
349 context = super(RecipesTable, self).get_context_data(**kwargs)
350
351 context['project'] = project
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600352 context['projectlayers'] = [player.layercommit.id for player in ProjectLayer.objects.filter(project=context['project'])]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500353
354 return context
355
356 def setup_filters(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500357 table_filter = TableFilter(
358 'in_current_project',
359 'Filter by project recipes'
360 )
361
362 in_project_action = TableFilterActionToggle(
363 'in_project',
364 'Recipes provided by layers added to this project',
365 ProjectFilters.in_project(self.project_layers)
366 )
367
368 not_in_project_action = TableFilterActionToggle(
369 'not_in_project',
370 'Recipes provided by layers not added to this project',
371 ProjectFilters.not_in_project(self.project_layers)
372 )
373
374 table_filter.add_action(in_project_action)
375 table_filter.add_action(not_in_project_action)
376 self.add_filter(table_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500377
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500378 def setup_queryset(self, *args, **kwargs):
379 prj = Project.objects.get(pk = kwargs['pid'])
380
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500381 # Project layers used by the filters
382 self.project_layers = prj.get_project_layer_versions(pk=True)
383
384 # Project layers used to switch the button states
385 self.static_context_extra['current_layers'] = self.project_layers
386
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500387 self.queryset = prj.get_all_compatible_recipes()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500388
389
390 def setup_columns(self, *args, **kwargs):
391
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500392 self.add_column(title="Version",
393 hidden=False,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500394 field_name="version")
395
396 self.add_column(title="Description",
397 field_name="get_description_or_summary")
398
399 recipe_file_template = '''
400 <code>{{data.file_path}}</code>
401 <a href="{{data.get_vcs_recipe_file_link_url}}" target="_blank">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600402 <span class="glyphicon glyphicon-new-window"></i>
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500403 </a>
404 '''
405
406 self.add_column(title="Recipe file",
407 help_text="Path to the recipe .bb file",
408 hidden=True,
409 static_data_name="recipe-file",
410 static_data_template=recipe_file_template)
411
412 self.add_column(title="Section",
413 help_text="The section in which recipes should be categorized",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500414 hidden=True,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500415 orderable=True,
416 field_name="section")
417
418 layer_link_template = '''
419 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
420 {{data.layer_version.layer.name}}</a>
421 '''
422
423 self.add_column(title="Layer",
424 help_text="The name of the layer providing the recipe",
425 orderable=True,
426 static_data_name="layer_version__layer__name",
427 static_data_template=layer_link_template)
428
429 self.add_column(title="License",
430 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 -0500431 hidden=True,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500432 orderable=True,
433 field_name="license")
434
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600435 revision_link_template = '''
436 {% if data.layer_version.layer.local_source_dir %}
437 <span class="text-muted">Not applicable</span>
438 <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>
439 {% else %}
440 {{data.layer_version.get_vcs_reference}}
441 {% endif %}
442 '''
443
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500444 self.add_column(title="Git revision",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500445 hidden=True,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600446 static_data_name="layer_version__get_vcs_reference",
447 static_data_template=revision_link_template)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500448
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500449
450class LayerRecipesTable(RecipesTable):
451 """ Smaller version of the Recipes table for use in layer details """
452
453 def __init__(self, *args, **kwargs):
454 super(LayerRecipesTable, self).__init__(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500455 self.default_orderby = "name"
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500456
457 def get_context_data(self, **kwargs):
458 context = super(LayerRecipesTable, self).get_context_data(**kwargs)
459 context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
460 return context
461
462
463 def setup_queryset(self, *args, **kwargs):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500464 self.queryset = \
465 Recipe.objects.filter(layer_version__pk=int(kwargs['layerid']))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500466
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500467 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500468 self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
469
470 def setup_columns(self, *args, **kwargs):
471 self.add_column(title="Recipe",
472 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",
473 hideable=False,
474 orderable=True,
475 field_name="name")
476
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500477 self.add_column(title="Version",
478 field_name="version")
479
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500480 self.add_column(title="Description",
481 field_name="get_description_or_summary")
482
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600483 build_recipe_template = '''
484 <a class="btn btn-default btn-block build-recipe-btn
485 {% if extra.in_prj == 0 %}disabled{% endif %}"
486 data-recipe-name="{{data.name}}">Build recipe</a>
487 '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500488
489 self.add_column(title="Build recipe",
490 static_data_name="add-del-layers",
491 static_data_template=build_recipe_template)
492
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500493class CustomImagesTable(ToasterTable):
494 """ Table to display your custom images """
495 def __init__(self, *args, **kwargs):
496 super(CustomImagesTable, self).__init__(*args, **kwargs)
497 self.title = "Custom images"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500498 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500499
500 def get_context_data(self, **kwargs):
501 context = super(CustomImagesTable, self).get_context_data(**kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600502
503 empty_state_template = '''
504 You have not created any custom images yet.
505 <a href="{% url 'newcustomimage' data.pid %}">
506 Create your first custom image</a>
507 '''
508 context['empty_state'] = self.render_static_data(empty_state_template,
509 kwargs)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500510 project = Project.objects.get(pk=kwargs['pid'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600511
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500512 # TODO put project into the ToasterTable base class
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500513 context['project'] = project
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500514 return context
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500515
516 def setup_queryset(self, *args, **kwargs):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500517 prj = Project.objects.get(pk = kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500518 self.queryset = CustomImageRecipe.objects.filter(project=prj)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500519 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500520
521 def setup_columns(self, *args, **kwargs):
522
523 name_link_template = '''
524 <a href="{% url 'customrecipe' extra.pid data.id %}">
525 {{data.name}}
526 </a>
527 '''
528
529 self.add_column(title="Custom image",
530 hideable=False,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500531 orderable=True,
532 field_name="name",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500533 static_data_name="name",
534 static_data_template=name_link_template)
535
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500536 recipe_file_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600537 {% if data.get_base_recipe_file %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500538 <code>{{data.name}}_{{data.version}}.bb</code>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600539 <a href="{% url 'customrecipedownload' extra.pid data.pk %}"
540 class="glyphicon glyphicon-download-alt get-help" title="Download recipe file"></a>
541 {% endif %}'''
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500542
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500543 self.add_column(title="Recipe file",
544 static_data_name='recipe_file_download',
545 static_data_template=recipe_file_template)
546
547 approx_packages_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600548 {% if data.get_all_packages.count > 0 %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500549 <a href="{% url 'customrecipe' extra.pid data.id %}">
550 {{data.get_all_packages.count}}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600551 </a>
552 {% endif %}'''
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500553
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600554 self.add_column(title="Packages",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500555 static_data_name='approx_packages',
556 static_data_template=approx_packages_template)
557
558
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500559 build_btn_template = '''
560 <button data-recipe-name="{{data.name}}"
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600561 class="btn btn-default btn-block build-recipe-btn">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500562 Build
563 </button>'''
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500564
565 self.add_column(title="Build",
566 hideable=False,
567 static_data_name='build_custom_img',
568 static_data_template=build_btn_template)
569
570class ImageRecipesTable(RecipesTable):
571 """ A subset of the recipes table which displayed just image recipes """
572
573 def __init__(self, *args, **kwargs):
574 super(ImageRecipesTable, self).__init__(*args, **kwargs)
575 self.title = "Compatible image recipes"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500576 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500577
578 def setup_queryset(self, *args, **kwargs):
579 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
580
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500581 custom_image_recipes = CustomImageRecipe.objects.filter(
582 project=kwargs['pid'])
583 self.queryset = self.queryset.filter(
584 Q(is_image=True) & ~Q(pk__in=custom_image_recipes))
585 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500586
587
588 def setup_columns(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500589
590 name_link_template = '''
591 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
592 '''
593
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500594 self.add_column(title="Image recipe",
595 help_text="When you build an image recipe, you get an "
596 "image: a root file system you can"
597 "deploy to a machine",
598 hideable=False,
599 orderable=True,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500600 static_data_name="name",
601 static_data_template=name_link_template,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500602 field_name="name")
603
604 super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
605
606 self.add_column(**RecipesTable.build_col)
607
608
609class NewCustomImagesTable(ImageRecipesTable):
610 """ Table which displays Images recipes which can be customised """
611 def __init__(self, *args, **kwargs):
612 super(NewCustomImagesTable, self).__init__(*args, **kwargs)
613 self.title = "Select the image recipe you want to customise"
614
615 def setup_queryset(self, *args, **kwargs):
616 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500617 prj = Project.objects.get(pk = kwargs['pid'])
618 self.static_context_extra['current_layers'] = \
619 prj.get_project_layer_versions(pk=True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500620
621 self.queryset = self.queryset.filter(is_image=True)
622
623 def setup_columns(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500624
625 name_link_template = '''
626 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
627 '''
628
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500629 self.add_column(title="Image recipe",
630 help_text="When you build an image recipe, you get an "
631 "image: a root file system you can"
632 "deploy to a machine",
633 hideable=False,
634 orderable=True,
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500635 static_data_name="name",
636 static_data_template=name_link_template,
637 field_name="name")
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500638
639 super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
640
641 self.add_column(title="Customise",
642 hideable=False,
643 filter_name="in_current_project",
644 static_data_name="customise-or-add-recipe",
645 static_data_template='{% include "customise_btn.html" %}')
646
647
648class SoftwareRecipesTable(RecipesTable):
649 """ Displays just the software recipes """
650 def __init__(self, *args, **kwargs):
651 super(SoftwareRecipesTable, self).__init__(*args, **kwargs)
652 self.title = "Compatible software recipes"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500653 self.default_orderby = "name"
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500654
655 def setup_queryset(self, *args, **kwargs):
656 super(SoftwareRecipesTable, self).setup_queryset(*args, **kwargs)
657
658 self.queryset = self.queryset.filter(is_image=False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500659 self.queryset = self.queryset.order_by(self.default_orderby)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500660
661
662 def setup_columns(self, *args, **kwargs):
663 self.add_column(title="Software recipe",
664 help_text="Information about a single piece of "
665 "software, including where to download the source, "
666 "configuration options, how to compile the source "
667 "files and how to package the compiled output",
668 hideable=False,
669 orderable=True,
670 field_name="name")
671
672 super(SoftwareRecipesTable, self).setup_columns(*args, **kwargs)
673
674 self.add_column(**RecipesTable.build_col)
675
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500676class PackagesTable(ToasterTable):
677 """ Table to display the packages in a recipe from it's last successful
678 build"""
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500679
680 def __init__(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500681 super(PackagesTable, self).__init__(*args, **kwargs)
682 self.title = "Packages included"
683 self.packages = None
684 self.default_orderby = "name"
685
686 def create_package_list(self, recipe, project_id):
687 """Creates a list of packages for the specified recipe by looking for
688 the last SUCCEEDED build of ther recipe"""
689
690 target = Target.objects.filter(Q(target=recipe.name) &
691 Q(build__project_id=project_id) &
692 Q(build__outcome=Build.SUCCEEDED)
693 ).last()
694
695 if target:
696 pkgs = target.target_installed_package_set.values_list('package',
697 flat=True)
698 return Package.objects.filter(pk__in=pkgs)
699
700 # Target/recipe never successfully built so empty queryset
701 return Package.objects.none()
702
703 def get_context_data(self, **kwargs):
704 """Context for rendering the sidebar and other items on the recipe
705 details page """
706 context = super(PackagesTable, self).get_context_data(**kwargs)
707
708 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
709 project = Project.objects.get(pk=kwargs['pid'])
710
711 in_project = (recipe.layer_version.pk in
712 project.get_project_layer_versions(pk=True))
713
714 packages = self.create_package_list(recipe, project.pk)
715
716 context.update({'project': project,
717 'recipe' : recipe,
718 'packages': packages,
719 'approx_pkg_size' : packages.aggregate(Sum('size')),
720 'in_project' : in_project,
721 })
722
723 return context
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500724
725 def setup_queryset(self, *args, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500726 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600727 self.static_context_extra['target_name'] = recipe.name
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500728
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500729 self.queryset = self.create_package_list(recipe, kwargs['pid'])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500730 self.queryset = self.queryset.order_by('name')
731
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500732 def setup_columns(self, *args, **kwargs):
733 self.add_column(title="Package",
734 hideable=False,
735 orderable=True,
736 field_name="name")
737
738 self.add_column(title="Package Version",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500739 field_name="version",
740 hideable=False)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500741
742 self.add_column(title="Approx Size",
743 orderable=True,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600744 field_name="size",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500745 static_data_name="size",
746 static_data_template="{% load projecttags %} \
747 {{data.size|filtered_filesizeformat}}")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500748
749 self.add_column(title="License",
750 field_name="license",
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600751 orderable=True,
752 hidden=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500753
754
755 self.add_column(title="Dependencies",
756 static_data_name="dependencies",
757 static_data_template='\
758 {% include "snippets/pkg_dependencies_popover.html" %}')
759
760 self.add_column(title="Reverse dependencies",
761 static_data_name="reverse_dependencies",
762 static_data_template='\
763 {% include "snippets/pkg_revdependencies_popover.html" %}',
764 hidden=True)
765
766 self.add_column(title="Recipe",
767 field_name="recipe__name",
768 orderable=True,
769 hidden=True)
770
771 self.add_column(title="Recipe version",
772 field_name="recipe__version",
773 hidden=True)
774
775
776class SelectPackagesTable(PackagesTable):
777 """ Table to display the packages to add and remove from an image """
778
779 def __init__(self, *args, **kwargs):
780 super(SelectPackagesTable, self).__init__(*args, **kwargs)
781 self.title = "Add | Remove packages"
782
783 def setup_queryset(self, *args, **kwargs):
784 self.cust_recipe =\
785 CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
786 prj = Project.objects.get(pk = kwargs['pid'])
787
788 current_packages = self.cust_recipe.get_all_packages()
789
790 current_recipes = prj.get_available_recipes()
791
792 # only show packages where recipes->layers are in the project
793 self.queryset = CustomImagePackage.objects.filter(
794 ~Q(recipe=None) &
795 Q(recipe__in=current_recipes))
796
797 self.queryset = self.queryset.order_by('name')
798
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600799 # This target is the target used to work out which group of dependences
800 # to display, if we've built the custom image we use it otherwise we
801 # can use the based recipe instead
802 if prj.build_set.filter(target__target=self.cust_recipe.name).count()\
803 > 0:
804 self.static_context_extra['target_name'] = self.cust_recipe.name
805 else:
806 self.static_context_extra['target_name'] =\
807 Package_DependencyManager.TARGET_LATEST
808
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500809 self.static_context_extra['recipe_id'] = kwargs['custrecipeid']
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600810
811
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500812 self.static_context_extra['current_packages'] = \
813 current_packages.values_list('pk', flat=True)
814
815 def get_context_data(self, **kwargs):
816 # to reuse the Super class map the custrecipeid to the recipe_id
817 kwargs['recipe_id'] = kwargs['custrecipeid']
818 context = super(SelectPackagesTable, self).get_context_data(**kwargs)
819 custom_recipe = \
820 CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
821
822 context['recipe'] = custom_recipe
823 context['approx_pkg_size'] = \
824 custom_recipe.get_all_packages().aggregate(Sum('size'))
825 return context
826
827
828 def setup_columns(self, *args, **kwargs):
829 super(SelectPackagesTable, self).setup_columns(*args, **kwargs)
830
831 add_remove_template = '{% include "pkg_add_rm_btn.html" %}'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500832
833 self.add_column(title="Add | Remove",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500834 hideable=False,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500835 help_text="Use the add and remove buttons to modify "
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500836 "the package content of your custom image",
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500837 static_data_name="add_rm_pkg_btn",
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500838 static_data_template=add_remove_template,
839 filter_name='in_current_image_filter')
840
841 def setup_filters(self, *args, **kwargs):
842 in_current_image_filter = TableFilter(
843 'in_current_image_filter',
844 'Filter by added packages'
845 )
846
847 in_image_action = TableFilterActionToggle(
848 'in_image',
849 'Packages in %s' % self.cust_recipe.name,
850 Q(pk__in=self.static_context_extra['current_packages'])
851 )
852
853 not_in_image_action = TableFilterActionToggle(
854 'not_in_image',
855 'Packages not added to %s' % self.cust_recipe.name,
856 ~Q(pk__in=self.static_context_extra['current_packages'])
857 )
858
859 in_current_image_filter.add_action(in_image_action)
860 in_current_image_filter.add_action(not_in_image_action)
861 self.add_filter(in_current_image_filter)
862
863class ProjectsTable(ToasterTable):
864 """Table of projects in Toaster"""
865
866 def __init__(self, *args, **kwargs):
867 super(ProjectsTable, self).__init__(*args, **kwargs)
868 self.default_orderby = '-updated'
869 self.title = 'All projects'
870 self.static_context_extra['Build'] = Build
871
872 def get_context_data(self, **kwargs):
873 return super(ProjectsTable, self).get_context_data(**kwargs)
874
875 def setup_queryset(self, *args, **kwargs):
876 queryset = Project.objects.all()
877
878 # annotate each project with its number of builds
879 queryset = queryset.annotate(num_builds=Count('build'))
880
881 # exclude the command line builds project if it has no builds
882 q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0)
883 queryset = queryset.filter(Q(is_default=False) |
884 q_default_with_builds)
885
886 # order rows
887 queryset = queryset.order_by(self.default_orderby)
888
889 self.queryset = queryset
890
891 # columns: last activity on (updated) - DEFAULT, project (name), release,
892 # machine, number of builds, last build outcome, recipe (name), errors,
893 # warnings, image files
894 def setup_columns(self, *args, **kwargs):
895 name_template = '''
896 {% load project_url_tag %}
897 <span data-project-field="name">
898 <a href="{% project_url data %}">
899 {{data.name}}
900 </a>
901 </span>
902 '''
903
904 last_activity_on_template = '''
905 {% load project_url_tag %}
906 <span data-project-field="updated">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500907 {{data.updated | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500908 </span>
909 '''
910
911 release_template = '''
912 <span data-project-field="release">
913 {% if data.release %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600914 {{data.release.name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500915 {% elif data.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600916 <span class="text-muted">Not applicable</span>
917 <span class="glyphicon glyphicon-question-sign get-help hover-help"
918 title="This project does not have a release set.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500919 It simply collects information about the builds you start from
920 the command line while Toaster is running"
921 style="visibility: hidden;">
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600922 </span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500923 {% else %}
924 No release available
925 {% endif %}
926 </span>
927 '''
928
929 machine_template = '''
930 <span data-project-field="machine">
931 {% if data.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600932 <span class="text-muted">Not applicable</span>
933 <span class="glyphicon glyphicon-question-sign get-help hover-help"
934 title="This project does not have a machine
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500935 set. It simply collects information about the builds you
936 start from the command line while Toaster is running"
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600937 style="visibility: hidden;"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500938 {% else %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600939 {{data.get_current_machine_name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500940 {% endif %}
941 </span>
942 '''
943
944 number_of_builds_template = '''
945 {% if data.get_number_of_builds > 0 %}
946 <a href="{% url 'projectbuilds' data.id %}">
947 {{data.get_number_of_builds}}
948 </a>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500949 {% endif %}
950 '''
951
952 last_build_outcome_template = '''
953 {% if data.get_number_of_builds > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600954 {% if data.get_last_outcome == extra.Build.SUCCEEDED %}
955 <span class="glyphicon glyphicon-ok-circle"></span>
956 {% elif data.get_last_outcome == extra.Build.FAILED %}
957 <span class="glyphicon glyphicon-minus-sign"></span>
958 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500959 {% endif %}
960 '''
961
962 recipe_template = '''
963 {% if data.get_number_of_builds > 0 %}
964 <a href="{% url "builddashboard" data.get_last_build_id %}">
965 {{data.get_last_target}}
966 </a>
967 {% endif %}
968 '''
969
970 errors_template = '''
971 {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600972 <a class="errors.count text-danger"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500973 href="{% url "builddashboard" data.get_last_build_id %}#errors">
974 {{data.get_last_errors}} error{{data.get_last_errors | pluralize}}
975 </a>
976 {% endif %}
977 '''
978
979 warnings_template = '''
980 {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600981 <a class="warnings.count text-warning"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500982 href="{% url "builddashboard" data.get_last_build_id %}#warnings">
983 {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}}
984 </a>
985 {% endif %}
986 '''
987
988 image_files_template = '''
989 {% if data.get_number_of_builds > 0 and data.get_last_outcome == extra.Build.SUCCEEDED %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600990 {{data.get_last_build_extensions}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500991 {% endif %}
992 '''
993
994 self.add_column(title='Project',
995 hideable=False,
996 orderable=True,
997 static_data_name='name',
998 static_data_template=name_template)
999
1000 self.add_column(title='Last activity on',
1001 help_text='Starting date and time of the \
1002 last project build. If the project has no \
1003 builds, this shows the date the project was \
1004 created.',
1005 hideable=False,
1006 orderable=True,
1007 static_data_name='updated',
1008 static_data_template=last_activity_on_template)
1009
1010 self.add_column(title='Release',
1011 help_text='The version of the build system used by \
1012 the project',
1013 hideable=False,
1014 orderable=True,
1015 static_data_name='release',
1016 static_data_template=release_template)
1017
1018 self.add_column(title='Machine',
1019 help_text='The hardware currently selected for the \
1020 project',
1021 hideable=False,
1022 orderable=False,
1023 static_data_name='machine',
1024 static_data_template=machine_template)
1025
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001026 self.add_column(title='Builds',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001027 help_text='The number of builds which have been run \
1028 for the project',
1029 hideable=False,
1030 orderable=False,
1031 static_data_name='number_of_builds',
1032 static_data_template=number_of_builds_template)
1033
1034 self.add_column(title='Last build outcome',
1035 help_text='Indicates whether the last project build \
1036 completed successfully or failed',
1037 hideable=True,
1038 orderable=False,
1039 static_data_name='last_build_outcome',
1040 static_data_template=last_build_outcome_template)
1041
1042 self.add_column(title='Recipe',
1043 help_text='The last recipe which was built in this \
1044 project',
1045 hideable=True,
1046 orderable=False,
1047 static_data_name='recipe_name',
1048 static_data_template=recipe_template)
1049
1050 self.add_column(title='Errors',
1051 help_text='The number of errors encountered during \
1052 the last project build (if any)',
1053 hideable=True,
1054 orderable=False,
1055 static_data_name='errors',
1056 static_data_template=errors_template)
1057
1058 self.add_column(title='Warnings',
1059 help_text='The number of warnings encountered during \
1060 the last project build (if any)',
1061 hideable=True,
1062 hidden=True,
1063 orderable=False,
1064 static_data_name='warnings',
1065 static_data_template=warnings_template)
1066
1067 self.add_column(title='Image files',
1068 help_text='The root file system types produced by \
1069 the last project build',
1070 hideable=True,
1071 hidden=True,
1072 orderable=False,
1073 static_data_name='image_files',
1074 static_data_template=image_files_template)
1075
1076class BuildsTable(ToasterTable):
1077 """Table of builds in Toaster"""
1078
1079 def __init__(self, *args, **kwargs):
1080 super(BuildsTable, self).__init__(*args, **kwargs)
1081 self.default_orderby = '-completed_on'
1082 self.static_context_extra['Build'] = Build
1083 self.static_context_extra['Task'] = Task
1084
1085 # attributes that are overridden in subclasses
1086
1087 # title for the page
1088 self.title = ''
1089
1090 # 'project' or 'all'; determines how the mrb (most recent builds)
1091 # section is displayed
1092 self.mrb_type = ''
1093
1094 def get_builds(self):
1095 """
1096 overridden in ProjectBuildsTable to return builds for a
1097 single project
1098 """
1099 return Build.objects.all()
1100
1101 def get_context_data(self, **kwargs):
1102 context = super(BuildsTable, self).get_context_data(**kwargs)
1103
1104 # should be set in subclasses
1105 context['mru'] = []
1106
1107 context['mrb_type'] = self.mrb_type
1108
1109 return context
1110
1111 def setup_queryset(self, *args, **kwargs):
1112 """
1113 The queryset is annotated so that it can be sorted by number of
1114 errors and number of warnings; but note that the criteria for
1115 finding the log messages to populate these fields should match those
1116 used in the Build model (orm/models.py) to populate the errors and
1117 warnings properties
1118 """
1119 queryset = self.get_builds()
1120
1121 # Don't include in progress builds pr cancelled builds
1122 queryset = queryset.exclude(Q(outcome=Build.IN_PROGRESS) |
1123 Q(outcome=Build.CANCELLED))
1124
1125 # sort
1126 queryset = queryset.order_by(self.default_orderby)
1127
1128 # annotate with number of ERROR, EXCEPTION and CRITICAL log messages
1129 criteria = (Q(logmessage__level=LogMessage.ERROR) |
1130 Q(logmessage__level=LogMessage.EXCEPTION) |
1131 Q(logmessage__level=LogMessage.CRITICAL))
1132
1133 queryset = queryset.annotate(
1134 errors_no=Count(
1135 Case(
1136 When(criteria, then=Value(1)),
1137 output_field=IntegerField()
1138 )
1139 )
1140 )
1141
1142 # annotate with number of WARNING log messages
1143 queryset = queryset.annotate(
1144 warnings_no=Count(
1145 Case(
1146 When(logmessage__level=LogMessage.WARNING, then=Value(1)),
1147 output_field=IntegerField()
1148 )
1149 )
1150 )
1151
1152 self.queryset = queryset
1153
1154 def setup_columns(self, *args, **kwargs):
1155 outcome_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001156 {% if data.outcome == data.SUCCEEDED %}
1157 <span class="glyphicon glyphicon-ok-circle"></span>
1158 {% elif data.outcome == data.FAILED %}
1159 <span class="glyphicon glyphicon-minus-sign"></span>
1160 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001161
1162 {% if data.cooker_log_path %}
1163 &nbsp;
1164 <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001165 <span class="glyphicon glyphicon-download-alt get-help"
1166 data-original-title="Download build log"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001167 </a>
1168 {% endif %}
1169 '''
1170
1171 recipe_template = '''
1172 {% for target_label in data.target_labels %}
1173 <a href="{% url "builddashboard" data.id %}">
1174 {{target_label}}
1175 </a>
1176 <br />
1177 {% endfor %}
1178 '''
1179
1180 machine_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001181 {{data.machine}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001182 '''
1183
1184 started_on_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001185 {{data.started_on | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001186 '''
1187
1188 completed_on_template = '''
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001189 {{data.completed_on | date:"d/m/y H:i"}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001190 '''
1191
1192 failed_tasks_template = '''
1193 {% if data.failed_tasks.count == 1 %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001194 <a class="text-danger" href="{% url "task" data.id data.failed_tasks.0.id %}">
1195 <span>
1196 {{data.failed_tasks.0.recipe.name}} {{data.failed_tasks.0.task_name}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001197 </span>
1198 </a>
1199 <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001200 <span class="glyphicon glyphicon-download-alt get-help"
1201 title="Download task log file">
1202 </span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001203 </a>
1204 {% elif data.failed_tasks.count > 1 %}
1205 <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}">
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001206 <span class="text-danger">{{data.failed_tasks.count}} tasks</span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001207 </a>
1208 {% endif %}
1209 '''
1210
1211 errors_template = '''
1212 {% if data.errors_no %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001213 <a class="errors.count text-danger" href="{% url "builddashboard" data.id %}#errors">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001214 {{data.errors_no}} error{{data.errors_no|pluralize}}
1215 </a>
1216 {% endif %}
1217 '''
1218
1219 warnings_template = '''
1220 {% if data.warnings_no %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001221 <a class="warnings.count text-warning" href="{% url "builddashboard" data.id %}#warnings">
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001222 {{data.warnings_no}} warning{{data.warnings_no|pluralize}}
1223 </a>
1224 {% endif %}
1225 '''
1226
1227 time_template = '''
1228 {% load projecttags %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001229 {% if data.outcome == extra.Build.SUCCEEDED %}
1230 <a href="{% url "buildtime" data.id %}">
1231 {{data.timespent_seconds | sectohms}}
1232 </a>
1233 {% else %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001234 {{data.timespent_seconds | sectohms}}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001235 {% endif %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001236 '''
1237
1238 image_files_template = '''
1239 {% if data.outcome == extra.Build.SUCCEEDED %}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001240 {{data.get_image_file_extensions}}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001241 {% endif %}
1242 '''
1243
1244 self.add_column(title='Outcome',
1245 help_text='Final state of the build (successful \
1246 or failed)',
1247 hideable=False,
1248 orderable=True,
1249 filter_name='outcome_filter',
1250 static_data_name='outcome',
1251 static_data_template=outcome_template)
1252
1253 self.add_column(title='Recipe',
1254 help_text='What was built (i.e. one or more recipes \
1255 or image recipes)',
1256 hideable=False,
1257 orderable=False,
1258 static_data_name='target',
1259 static_data_template=recipe_template)
1260
1261 self.add_column(title='Machine',
1262 help_text='Hardware for which you are building a \
1263 recipe or image recipe',
1264 hideable=False,
1265 orderable=True,
1266 static_data_name='machine',
1267 static_data_template=machine_template)
1268
1269 self.add_column(title='Started on',
1270 help_text='The date and time when the build started',
1271 hideable=True,
1272 hidden=True,
1273 orderable=True,
1274 filter_name='started_on_filter',
1275 static_data_name='started_on',
1276 static_data_template=started_on_template)
1277
1278 self.add_column(title='Completed on',
1279 help_text='The date and time when the build finished',
1280 hideable=False,
1281 orderable=True,
1282 filter_name='completed_on_filter',
1283 static_data_name='completed_on',
1284 static_data_template=completed_on_template)
1285
1286 self.add_column(title='Failed tasks',
1287 help_text='The number of tasks which failed during \
1288 the build',
1289 hideable=True,
1290 orderable=False,
1291 filter_name='failed_tasks_filter',
1292 static_data_name='failed_tasks',
1293 static_data_template=failed_tasks_template)
1294
1295 self.add_column(title='Errors',
1296 help_text='The number of errors encountered during \
1297 the build (if any)',
1298 hideable=True,
1299 orderable=True,
1300 static_data_name='errors_no',
1301 static_data_template=errors_template)
1302
1303 self.add_column(title='Warnings',
1304 help_text='The number of warnings encountered during \
1305 the build (if any)',
1306 hideable=True,
1307 orderable=True,
1308 static_data_name='warnings_no',
1309 static_data_template=warnings_template)
1310
1311 self.add_column(title='Time',
1312 help_text='How long the build took to finish',
1313 hideable=True,
1314 hidden=True,
1315 orderable=False,
1316 static_data_name='time',
1317 static_data_template=time_template)
1318
1319 self.add_column(title='Image files',
1320 help_text='The root file system types produced by \
1321 the build',
1322 hideable=True,
1323 orderable=False,
1324 static_data_name='image_files',
1325 static_data_template=image_files_template)
1326
1327 def setup_filters(self, *args, **kwargs):
1328 # outcomes
1329 outcome_filter = TableFilter(
1330 'outcome_filter',
1331 'Filter builds by outcome'
1332 )
1333
1334 successful_builds_action = TableFilterActionToggle(
1335 'successful_builds',
1336 'Successful builds',
1337 Q(outcome=Build.SUCCEEDED)
1338 )
1339
1340 failed_builds_action = TableFilterActionToggle(
1341 'failed_builds',
1342 'Failed builds',
1343 Q(outcome=Build.FAILED)
1344 )
1345
1346 outcome_filter.add_action(successful_builds_action)
1347 outcome_filter.add_action(failed_builds_action)
1348 self.add_filter(outcome_filter)
1349
1350 # started on
1351 started_on_filter = TableFilter(
1352 'started_on_filter',
1353 'Filter by date when build was started'
1354 )
1355
1356 started_today_action = TableFilterActionDay(
1357 'today',
1358 'Today\'s builds',
1359 'started_on',
1360 'today'
1361 )
1362
1363 started_yesterday_action = TableFilterActionDay(
1364 'yesterday',
1365 'Yesterday\'s builds',
1366 'started_on',
1367 'yesterday'
1368 )
1369
1370 by_started_date_range_action = TableFilterActionDateRange(
1371 'date_range',
1372 'Build date range',
1373 'started_on'
1374 )
1375
1376 started_on_filter.add_action(started_today_action)
1377 started_on_filter.add_action(started_yesterday_action)
1378 started_on_filter.add_action(by_started_date_range_action)
1379 self.add_filter(started_on_filter)
1380
1381 # completed on
1382 completed_on_filter = TableFilter(
1383 'completed_on_filter',
1384 'Filter by date when build was completed'
1385 )
1386
1387 completed_today_action = TableFilterActionDay(
1388 'today',
1389 'Today\'s builds',
1390 'completed_on',
1391 'today'
1392 )
1393
1394 completed_yesterday_action = TableFilterActionDay(
1395 'yesterday',
1396 'Yesterday\'s builds',
1397 'completed_on',
1398 'yesterday'
1399 )
1400
1401 by_completed_date_range_action = TableFilterActionDateRange(
1402 'date_range',
1403 'Build date range',
1404 'completed_on'
1405 )
1406
1407 completed_on_filter.add_action(completed_today_action)
1408 completed_on_filter.add_action(completed_yesterday_action)
1409 completed_on_filter.add_action(by_completed_date_range_action)
1410 self.add_filter(completed_on_filter)
1411
1412 # failed tasks
1413 failed_tasks_filter = TableFilter(
1414 'failed_tasks_filter',
1415 'Filter builds by failed tasks'
1416 )
1417
1418 criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
1419
1420 with_failed_tasks_action = TableFilterActionToggle(
1421 'with_failed_tasks',
1422 'Builds with failed tasks',
1423 criteria
1424 )
1425
1426 without_failed_tasks_action = TableFilterActionToggle(
1427 'without_failed_tasks',
1428 'Builds without failed tasks',
1429 ~criteria
1430 )
1431
1432 failed_tasks_filter.add_action(with_failed_tasks_action)
1433 failed_tasks_filter.add_action(without_failed_tasks_action)
1434 self.add_filter(failed_tasks_filter)
1435
1436
1437class AllBuildsTable(BuildsTable):
1438 """ Builds page for all builds """
1439
1440 def __init__(self, *args, **kwargs):
1441 super(AllBuildsTable, self).__init__(*args, **kwargs)
1442 self.title = 'All builds'
1443 self.mrb_type = 'all'
1444
1445 def setup_columns(self, *args, **kwargs):
1446 """
1447 All builds page shows a column for the project
1448 """
1449
1450 super(AllBuildsTable, self).setup_columns(*args, **kwargs)
1451
1452 project_template = '''
1453 {% load project_url_tag %}
1454 <a href="{% project_url data.project %}">
1455 {{data.project.name}}
1456 </a>
1457 {% if data.project.is_default %}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001458 <span class="glyphicon glyphicon-question-sign get-help hover-help" title=""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001459 data-original-title="This project shows information about
1460 the builds you start from the command line while Toaster is
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001461 running" style="visibility: hidden;"></span>
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001462 {% endif %}
1463 '''
1464
1465 self.add_column(title='Project',
1466 hideable=True,
1467 orderable=True,
1468 static_data_name='project',
1469 static_data_template=project_template)
1470
1471 def get_context_data(self, **kwargs):
1472 """ Get all builds for the recent builds area """
1473 context = super(AllBuildsTable, self).get_context_data(**kwargs)
1474 context['mru'] = Build.get_recent()
1475 return context
1476
1477class ProjectBuildsTable(BuildsTable):
1478 """
1479 Builds page for a single project; a BuildsTable, with the queryset
1480 filtered by project
1481 """
1482
1483 def __init__(self, *args, **kwargs):
1484 super(ProjectBuildsTable, self).__init__(*args, **kwargs)
1485 self.title = 'All project builds'
1486 self.mrb_type = 'project'
1487
1488 # set from the querystring
1489 self.project_id = None
1490
1491 def setup_columns(self, *args, **kwargs):
1492 """
1493 Project builds table doesn't show the machines column by default
1494 """
1495
1496 super(ProjectBuildsTable, self).setup_columns(*args, **kwargs)
1497
1498 # hide the machine column
1499 self.set_column_hidden('Machine', True)
1500
1501 # allow the machine column to be hidden by the user
1502 self.set_column_hideable('Machine', True)
1503
1504 def setup_queryset(self, *args, **kwargs):
1505 """
1506 NOTE: self.project_id must be set before calling super(),
1507 as it's used in setup_queryset()
1508 """
1509 self.project_id = kwargs['pid']
1510 super(ProjectBuildsTable, self).setup_queryset(*args, **kwargs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001511 project = Project.objects.get(pk=self.project_id)
1512 self.queryset = self.queryset.filter(project=project)
1513
1514 def get_context_data(self, **kwargs):
1515 """
1516 Get recent builds for this project, and the project itself
1517
1518 NOTE: self.project_id must be set before calling super(),
1519 as it's used in get_context_data()
1520 """
1521 self.project_id = kwargs['pid']
1522 context = super(ProjectBuildsTable, self).get_context_data(**kwargs)
1523
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001524 empty_state_template = '''
1525 This project has no builds.
1526 <a href="{% url 'projectimagerecipes' data.pid %}">
1527 Choose a recipe to build</a>
1528 '''
1529 context['empty_state'] = self.render_static_data(empty_state_template,
1530 kwargs)
1531
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001532 project = Project.objects.get(pk=self.project_id)
1533 context['mru'] = Build.get_recent(project)
1534 context['project'] = project
1535
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001536 self.setup_queryset(**kwargs)
1537 if self.queryset.count() == 0 and \
1538 project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0:
1539 context['build_in_progress_none_completed'] = True
1540 else:
1541 context['build_in_progress_none_completed'] = False
1542
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001543 return context
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001544
1545
1546class DistrosTable(ToasterTable):
1547 """Table of Distros in Toaster"""
1548
1549 def __init__(self, *args, **kwargs):
1550 super(DistrosTable, self).__init__(*args, **kwargs)
1551 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."
1552 self.title = "Compatible Distros"
1553 self.default_orderby = "name"
1554
1555 def get_context_data(self, **kwargs):
1556 context = super(DistrosTable, self).get_context_data(**kwargs)
1557 context['project'] = Project.objects.get(pk=kwargs['pid'])
1558 return context
1559
1560 def setup_filters(self, *args, **kwargs):
1561 project = Project.objects.get(pk=kwargs['pid'])
1562
1563 in_current_project_filter = TableFilter(
1564 "in_current_project",
1565 "Filter by project Distros"
1566 )
1567
1568 in_project_action = TableFilterActionToggle(
1569 "in_project",
1570 "Distro provided by layers added to this project",
1571 ProjectFilters.in_project(self.project_layers)
1572 )
1573
1574 not_in_project_action = TableFilterActionToggle(
1575 "not_in_project",
1576 "Distros provided by layers not added to this project",
1577 ProjectFilters.not_in_project(self.project_layers)
1578 )
1579
1580 in_current_project_filter.add_action(in_project_action)
1581 in_current_project_filter.add_action(not_in_project_action)
1582 self.add_filter(in_current_project_filter)
1583
1584 def setup_queryset(self, *args, **kwargs):
1585 prj = Project.objects.get(pk = kwargs['pid'])
1586 self.queryset = prj.get_all_compatible_distros()
1587 self.queryset = self.queryset.order_by(self.default_orderby)
1588
1589 self.static_context_extra['current_layers'] = \
1590 self.project_layers = \
1591 prj.get_project_layer_versions(pk=True)
1592
1593 def setup_columns(self, *args, **kwargs):
1594
1595 self.add_column(title="Distro",
1596 hideable=False,
1597 orderable=True,
1598 field_name="name")
1599
1600 self.add_column(title="Description",
1601 field_name="description")
1602
1603 layer_link_template = '''
1604 <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
1605 {{data.layer_version.layer.name}}</a>
1606 '''
1607
1608 self.add_column(title="Layer",
1609 static_data_name="layer_version__layer__name",
1610 static_data_template=layer_link_template,
1611 orderable=True)
1612
1613 self.add_column(title="Git revision",
1614 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",
1615 hidden=True,
1616 field_name="layer_version__get_vcs_reference")
1617
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001618 distro_file_template = '''<code>conf/distro/{{data.name}}.conf</code>
1619 {% 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 -05001620 self.add_column(title="Distro file",
1621 hidden=True,
1622 static_data_name="templatefile",
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001623 static_data_template=distro_file_template)
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001624
1625 self.add_column(title="Select",
1626 help_text="Sets the selected distro to the project",
1627 hideable=False,
1628 filter_name="in_current_project",
1629 static_data_name="add-del-layers",
1630 static_data_template='{% include "distro_btn.html" %}')
1631