blob: 9ff756bc8dfbcf5bdc07ed1ba01741c0b3b64844 [file] [log] [blame]
rjw1f884582022-01-06 17:20:42 +08001#
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
24from orm.models import CustomImageRecipe, Package, Target, Build, LogMessage, Task
25from orm.models import CustomImagePackage, Package_DependencyManager
26from orm.models import Distro
27from django.db.models import Q, Max, Sum, Count, When, Case, Value, IntegerField
28from django.conf.urls import url
29from django.core.urlresolvers import reverse, resolve
30from django.http import HttpResponse
31from django.views.generic import TemplateView
32
33from toastergui.tablefilter import TableFilter
34from toastergui.tablefilter import TableFilterActionToggle
35from toastergui.tablefilter import TableFilterActionDateRange
36from toastergui.tablefilter import TableFilterActionDay
37
38import os
39
40class ProjectFilters(object):
41 @staticmethod
42 def in_project(project_layers):
43 return Q(layer_version__in=project_layers)
44
45 @staticmethod
46 def not_in_project(project_layers):
47 return ~(ProjectFilters.in_project(project_layers))
48
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"
55 self.title = "Compatible layers"
56
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'])
61 context['project'] = project
62
63 return context
64
65 def setup_filters(self, *args, **kwargs):
66 project = Project.objects.get(pk=kwargs['pid'])
67 self.project_layers = ProjectLayer.objects.filter(project=project)
68
69 in_current_project_filter = TableFilter(
70 "in_current_project",
71 "Filter by project layers"
72 )
73
74 criteria = Q(projectlayer__in=self.project_layers)
75
76 in_project_action = TableFilterActionToggle(
77 "in_project",
78 "Layers added to this project",
79 criteria
80 )
81
82 not_in_project_action = TableFilterActionToggle(
83 "not_in_project",
84 "Layers not added to this project",
85 ~criteria
86 )
87
88 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)
91
92 def setup_queryset(self, *args, **kwargs):
93 prj = Project.objects.get(pk = kwargs['pid'])
94 compatible_layers = prj.get_all_compatible_layer_versions()
95
96 self.static_context_extra['current_layers'] = \
97 prj.get_project_layer_versions(pk=True)
98
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 %}">
120 {% if data.layer.local_source_dir %}
121 <code>{{data.layer.local_source_dir}}</code>
122 {% else %}
123 <code>{{data.layer.vcs_url}}</code>
124 </a>
125 {% endif %}
126 {% if data.get_vcs_link_url %}
127 <a target="_blank" href="{{ data.get_vcs_link_url }}">
128 <span class="glyphicon glyphicon-new-window"></span>
129 </a>
130 {% endif %}
131 '''
132
133 self.add_column(title="Layer source code location",
134 help_text="A Git repository or an absolute path to a directory",
135 hidden=True,
136 static_data_name="layer__vcs_url",
137 static_data_template=git_url_template)
138
139 git_dir_template = '''
140 {% 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 %}
144 <a href="{% url 'layerdetails' extra.pid data.id %}">
145 <code>{{data.dirpath}}</code>
146 </a>
147 {% endif %}
148 {% if data.dirpath and data.get_vcs_dirpath_link_url %}
149 <a target="_blank" href="{{ data.get_vcs_dirpath_link_url }}">
150 <span class="glyphicon glyphicon-new-window"></span>
151 </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 = '''
161 {% 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>
164 {% else %}
165 {% with vcs_ref=data.get_vcs_reference %}
166 {% include 'snippets/gitrev_popover.html' %}
167 {% endwith %}
168 {% endif %}
169 '''
170
171 self.add_column(title="Git revision",
172 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 %}
179 <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'>
181 {% 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
196 self.add_column(title="Add | Remove",
197 help_text="Add or remove layers to / from your project",
198 hideable=False,
199 filter_name="in_current_project",
200 static_data_name="add-del-layers",
201 static_data_template='{% include "layer_btn.html" %}')
202
203
204class MachinesTable(ToasterTable):
205 """Table of Machines in Toaster"""
206
207 def __init__(self, *args, **kwargs):
208 super(MachinesTable, self).__init__(*args, **kwargs)
209 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."
210 self.title = "Compatible machines"
211 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'])
216 return context
217
218 def setup_filters(self, *args, **kwargs):
219 project = Project.objects.get(pk=kwargs['pid'])
220
221 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)
241
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
247 self.static_context_extra['current_layers'] = \
248 self.project_layers = \
249 prj.get_project_layer_versions(pk=True)
250
251 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
271 self.add_column(title="Git revision",
272 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>
277 <a href="{{data.get_vcs_machine_file_link_url}}" target="_blank"><span class="glyphicon glyphicon-new-window"></i></a>'''
278
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']))
308 self.queryset = self.queryset.order_by(self.default_orderby)
309 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
320 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 '''
325
326 self.add_column(title="Select machine",
327 static_data_name="add-del-layers",
328 static_data_template=select_btn_template)
329
330
331class RecipesTable(ToasterTable):
332 """Table of All Recipes in Toaster"""
333
334 def __init__(self, *args, **kwargs):
335 super(RecipesTable, self).__init__(*args, **kwargs)
336 self.empty_state = "Toaster has no recipe information. To generate recipe information you need to run a build."
337
338 build_col = { 'title' : "Build",
339 'help_text' : "Before building a recipe, you might need to add the corresponding layer to your project",
340 'hideable' : False,
341 'filter_name' : "in_current_project",
342 'static_data_name' : "add-del-layers",
343 'static_data_template' : '{% include "recipe_btn.html" %}'}
344 if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
345 build_col['static_data_template'] = '{% include "recipe_add_btn.html" %}'
346
347 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
352 context['projectlayers'] = [player.layercommit.id for player in ProjectLayer.objects.filter(project=context['project'])]
353
354 return context
355
356 def setup_filters(self, *args, **kwargs):
357 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)
377
378 def setup_queryset(self, *args, **kwargs):
379 prj = Project.objects.get(pk = kwargs['pid'])
380
381 # 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
387 self.queryset = prj.get_all_compatible_recipes()
388
389
390 def setup_columns(self, *args, **kwargs):
391
392 self.add_column(title="Version",
393 hidden=False,
394 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">
402 <span class="glyphicon glyphicon-new-window"></i>
403 </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",
414 hidden=True,
415 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",
431 hidden=True,
432 orderable=True,
433 field_name="license")
434
435 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
444 self.add_column(title="Git revision",
445 hidden=True,
446 static_data_name="layer_version__get_vcs_reference",
447 static_data_template=revision_link_template)
448
449
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)
455 self.default_orderby = "name"
456
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):
464 self.queryset = \
465 Recipe.objects.filter(layer_version__pk=int(kwargs['layerid']))
466
467 self.queryset = self.queryset.order_by(self.default_orderby)
468 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
477 self.add_column(title="Version",
478 field_name="version")
479
480 self.add_column(title="Description",
481 field_name="get_description_or_summary")
482
483 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 '''
488
489 self.add_column(title="Build recipe",
490 static_data_name="add-del-layers",
491 static_data_template=build_recipe_template)
492
493class 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"
498 self.default_orderby = "name"
499
500 def get_context_data(self, **kwargs):
501 context = super(CustomImagesTable, self).get_context_data(**kwargs)
502
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)
510 project = Project.objects.get(pk=kwargs['pid'])
511
512 # TODO put project into the ToasterTable base class
513 context['project'] = project
514 return context
515
516 def setup_queryset(self, *args, **kwargs):
517 prj = Project.objects.get(pk = kwargs['pid'])
518 self.queryset = CustomImageRecipe.objects.filter(project=prj)
519 self.queryset = self.queryset.order_by(self.default_orderby)
520
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,
531 orderable=True,
532 field_name="name",
533 static_data_name="name",
534 static_data_template=name_link_template)
535
536 recipe_file_template = '''
537 {% if data.get_base_recipe_file %}
538 <code>{{data.name}}_{{data.version}}.bb</code>
539 <a href="{% url 'customrecipedownload' extra.pid data.pk %}"
540 class="glyphicon glyphicon-download-alt get-help" title="Download recipe file"></a>
541 {% endif %}'''
542
543 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 = '''
548 {% if data.get_all_packages.count > 0 %}
549 <a href="{% url 'customrecipe' extra.pid data.id %}">
550 {{data.get_all_packages.count}}
551 </a>
552 {% endif %}'''
553
554 self.add_column(title="Packages",
555 static_data_name='approx_packages',
556 static_data_template=approx_packages_template)
557
558
559 build_btn_template = '''
560 <button data-recipe-name="{{data.name}}"
561 class="btn btn-default btn-block build-recipe-btn">
562 Build
563 </button>'''
564
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"
576 self.default_orderby = "name"
577
578 def setup_queryset(self, *args, **kwargs):
579 super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
580
581 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)
586
587
588 def setup_columns(self, *args, **kwargs):
589
590 name_link_template = '''
591 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
592 '''
593
594 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,
600 static_data_name="name",
601 static_data_template=name_link_template,
602 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)
617 prj = Project.objects.get(pk = kwargs['pid'])
618 self.static_context_extra['current_layers'] = \
619 prj.get_project_layer_versions(pk=True)
620
621 self.queryset = self.queryset.filter(is_image=True)
622
623 def setup_columns(self, *args, **kwargs):
624
625 name_link_template = '''
626 <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
627 '''
628
629 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,
635 static_data_name="name",
636 static_data_template=name_link_template,
637 field_name="name")
638
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"
653 self.default_orderby = "name"
654
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)
659 self.queryset = self.queryset.order_by(self.default_orderby)
660
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
676class PackagesTable(ToasterTable):
677 """ Table to display the packages in a recipe from it's last successful
678 build"""
679
680 def __init__(self, *args, **kwargs):
681 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
724
725 def setup_queryset(self, *args, **kwargs):
726 recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
727 self.static_context_extra['target_name'] = recipe.name
728
729 self.queryset = self.create_package_list(recipe, kwargs['pid'])
730 self.queryset = self.queryset.order_by('name')
731
732 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",
739 field_name="version",
740 hideable=False)
741
742 self.add_column(title="Approx Size",
743 orderable=True,
744 field_name="size",
745 static_data_name="size",
746 static_data_template="{% load projecttags %} \
747 {{data.size|filtered_filesizeformat}}")
748
749 self.add_column(title="License",
750 field_name="license",
751 orderable=True,
752 hidden=True)
753
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
799 # 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
809 self.static_context_extra['recipe_id'] = kwargs['custrecipeid']
810
811
812 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" %}'
832
833 self.add_column(title="Add | Remove",
834 hideable=False,
835 help_text="Use the add and remove buttons to modify "
836 "the package content of your custom image",
837 static_data_name="add_rm_pkg_btn",
838 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">
907 {{data.updated | date:"d/m/y H:i"}}
908 </span>
909 '''
910
911 release_template = '''
912 <span data-project-field="release">
913 {% if data.release %}
914 {{data.release.name}}
915 {% elif data.is_default %}
916 <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.
919 It simply collects information about the builds you start from
920 the command line while Toaster is running"
921 style="visibility: hidden;">
922 </span>
923 {% 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 %}
932 <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
935 set. It simply collects information about the builds you
936 start from the command line while Toaster is running"
937 style="visibility: hidden;"></span>
938 {% else %}
939 {{data.get_current_machine_name}}
940 {% 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>
949 {% endif %}
950 '''
951
952 last_build_outcome_template = '''
953 {% if data.get_number_of_builds > 0 %}
954 {% 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 %}
959 {% 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 %}
972 <a class="errors.count text-danger"
973 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 %}
981 <a class="warnings.count text-warning"
982 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 %}
990 {{data.get_last_build_extensions}}
991 {% 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
1026 self.add_column(title='Builds',
1027 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 = '''
1156 {% 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 %}
1161
1162 {% if data.cooker_log_path %}
1163 &nbsp;
1164 <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
1165 <span class="glyphicon glyphicon-download-alt get-help"
1166 data-original-title="Download build log"></span>
1167 </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 = '''
1181 {{data.machine}}
1182 '''
1183
1184 started_on_template = '''
1185 {{data.started_on | date:"d/m/y H:i"}}
1186 '''
1187
1188 completed_on_template = '''
1189 {{data.completed_on | date:"d/m/y H:i"}}
1190 '''
1191
1192 failed_tasks_template = '''
1193 {% if data.failed_tasks.count == 1 %}
1194 <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}}
1197 </span>
1198 </a>
1199 <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}">
1200 <span class="glyphicon glyphicon-download-alt get-help"
1201 title="Download task log file">
1202 </span>
1203 </a>
1204 {% elif data.failed_tasks.count > 1 %}
1205 <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}">
1206 <span class="text-danger">{{data.failed_tasks.count}} tasks</span>
1207 </a>
1208 {% endif %}
1209 '''
1210
1211 errors_template = '''
1212 {% if data.errors_no %}
1213 <a class="errors.count text-danger" href="{% url "builddashboard" data.id %}#errors">
1214 {{data.errors_no}} error{{data.errors_no|pluralize}}
1215 </a>
1216 {% endif %}
1217 '''
1218
1219 warnings_template = '''
1220 {% if data.warnings_no %}
1221 <a class="warnings.count text-warning" href="{% url "builddashboard" data.id %}#warnings">
1222 {{data.warnings_no}} warning{{data.warnings_no|pluralize}}
1223 </a>
1224 {% endif %}
1225 '''
1226
1227 time_template = '''
1228 {% load projecttags %}
1229 {% if data.outcome == extra.Build.SUCCEEDED %}
1230 <a href="{% url "buildtime" data.id %}">
1231 {{data.timespent_seconds | sectohms}}
1232 </a>
1233 {% else %}
1234 {{data.timespent_seconds | sectohms}}
1235 {% endif %}
1236 '''
1237
1238 image_files_template = '''
1239 {% if data.outcome == extra.Build.SUCCEEDED %}
1240 {{data.get_image_file_extensions}}
1241 {% 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 %}
1458 <span class="glyphicon glyphicon-question-sign get-help hover-help" title=""
1459 data-original-title="This project shows information about
1460 the builds you start from the command line while Toaster is
1461 running" style="visibility: hidden;"></span>
1462 {% 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)
1511 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
1524 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
1532 project = Project.objects.get(pk=self.project_id)
1533 context['mru'] = Build.get_recent(project)
1534 context['project'] = project
1535
1536 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
1543 return context
1544
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
1618 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 %}'''
1620 self.add_column(title="Distro file",
1621 hidden=True,
1622 static_data_name="templatefile",
1623 static_data_template=distro_file_template)
1624
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