1#
2# BitBake Toaster Implementation
3#
4# Copyright (C) 2015        Intel Corporation
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9from toastergui.widgets import ToasterTable
10from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
11from orm.models import CustomImageRecipe, Package, Target, Build, LogMessage, Task
12from orm.models import CustomImagePackage, Package_DependencyManager
13from orm.models import Distro
14from django.db.models import Q, Max, Sum, Count, When, Case, Value, IntegerField
15from django.conf.urls import url
16from django.core.urlresolvers import reverse, resolve
17from django.http import HttpResponse
18from django.views.generic import TemplateView
19
20from toastergui.tablefilter import TableFilter
21from toastergui.tablefilter import TableFilterActionToggle
22from toastergui.tablefilter import TableFilterActionDateRange
23from toastergui.tablefilter import TableFilterActionDay
24
25import os
26
27class ProjectFilters(object):
28    @staticmethod
29    def in_project(project_layers):
30        return Q(layer_version__in=project_layers)
31
32    @staticmethod
33    def not_in_project(project_layers):
34        return ~(ProjectFilters.in_project(project_layers))
35
36class LayersTable(ToasterTable):
37    """Table of layers in Toaster"""
38
39    def __init__(self, *args, **kwargs):
40        super(LayersTable, self).__init__(*args, **kwargs)
41        self.default_orderby = "layer__name"
42        self.title = "Compatible layers"
43
44    def get_context_data(self, **kwargs):
45        context = super(LayersTable, self).get_context_data(**kwargs)
46
47        project = Project.objects.get(pk=kwargs['pid'])
48        context['project'] = project
49
50        return context
51
52    def setup_filters(self, *args, **kwargs):
53        project = Project.objects.get(pk=kwargs['pid'])
54        self.project_layers = ProjectLayer.objects.filter(project=project)
55
56        in_current_project_filter = TableFilter(
57            "in_current_project",
58            "Filter by project layers"
59        )
60
61        criteria = Q(projectlayer__in=self.project_layers)
62
63        in_project_action = TableFilterActionToggle(
64            "in_project",
65            "Layers added to this project",
66            criteria
67        )
68
69        not_in_project_action = TableFilterActionToggle(
70            "not_in_project",
71            "Layers not added to this project",
72            ~criteria
73        )
74
75        in_current_project_filter.add_action(in_project_action)
76        in_current_project_filter.add_action(not_in_project_action)
77        self.add_filter(in_current_project_filter)
78
79    def setup_queryset(self, *args, **kwargs):
80        prj = Project.objects.get(pk = kwargs['pid'])
81        compatible_layers = prj.get_all_compatible_layer_versions()
82
83        self.static_context_extra['current_layers'] = \
84                prj.get_project_layer_versions(pk=True)
85
86        self.queryset = compatible_layers.order_by(self.default_orderby)
87
88    def setup_columns(self, *args, **kwargs):
89
90        layer_link_template = '''
91        <a href="{% url 'layerdetails' extra.pid data.id %}">
92          {{data.layer.name}}
93        </a>
94        '''
95
96        self.add_column(title="Layer",
97                        hideable=False,
98                        orderable=True,
99                        static_data_name="layer__name",
100                        static_data_template=layer_link_template)
101
102        self.add_column(title="Summary",
103                        field_name="layer__summary")
104
105        git_url_template = '''
106        <a href="{% url 'layerdetails' extra.pid data.id %}">
107        {% if data.layer.local_source_dir %}
108          <code>{{data.layer.local_source_dir}}</code>
109        {% else %}
110          <code>{{data.layer.vcs_url}}</code>
111        </a>
112        {% endif %}
113        {% if data.get_vcs_link_url %}
114        <a target="_blank" href="{{ data.get_vcs_link_url }}">
115           <span class="glyphicon glyphicon-new-window"></span>
116        </a>
117        {% endif %}
118        '''
119
120        self.add_column(title="Layer source code location",
121                        help_text="A Git repository or an absolute path to a directory",
122                        hidden=True,
123                        static_data_name="layer__vcs_url",
124                        static_data_template=git_url_template)
125
126        git_dir_template = '''
127        {% if data.layer.local_source_dir %}
128        <span class="text-muted">Not applicable</span>
129        <span class="glyphicon glyphicon-question-sign get-help" data-original-title="" title="The source code of {{data.layer.name}} is not in a Git repository, so there is no subdirectory associated with it"> </span>
130        {% else %}
131        <a href="{% url 'layerdetails' extra.pid data.id %}">
132         <code>{{data.dirpath}}</code>
133        </a>
134        {% endif %}
135        {% if data.dirpath and data.get_vcs_dirpath_link_url %}
136        <a target="_blank" href="{{ data.get_vcs_dirpath_link_url }}">
137          <span class="glyphicon glyphicon-new-window"></span>
138        </a>
139        {% endif %}'''
140
141        self.add_column(title="Subdirectory",
142                        help_text="The layer directory within the Git repository",
143                        hidden=True,
144                        static_data_name="git_subdir",
145                        static_data_template=git_dir_template)
146
147        revision_template =  '''
148        {% if data.layer.local_source_dir %}
149        <span class="text-muted">Not applicable</span>
150        <span class="glyphicon glyphicon-question-sign get-help" data-original-title="" title="The source code of {{data.layer.name}} is not in a Git repository, so there is no revision associated with it"> </span>
151        {% else %}
152        {% with vcs_ref=data.get_vcs_reference %}
153        {% include 'snippets/gitrev_popover.html' %}
154        {% endwith %}
155        {% endif %}
156        '''
157
158        self.add_column(title="Git revision",
159                        help_text="The Git branch, tag or commit. For the layers from the OpenEmbedded layer source, the revision is always the branch compatible with the Yocto Project version you selected for this project",
160                        static_data_name="revision",
161                        static_data_template=revision_template)
162
163        deps_template = '''
164        {% with ods=data.dependencies.all%}
165        {% if ods.count %}
166            <a class="btn btn-default" title="<a href='{% url "layerdetails" extra.pid data.id %}'>{{data.layer.name}}</a> dependencies"
167        data-content="<ul class='list-unstyled'>
168        {% for i in ods%}
169        <li><a href='{% url "layerdetails" extra.pid i.depends_on.pk %}'>{{i.depends_on.layer.name}}</a></li>
170        {% endfor %}
171        </ul>">
172        {{ods.count}}
173        </a>
174        {% endif %}
175        {% endwith %}
176        '''
177
178        self.add_column(title="Dependencies",
179                        help_text="Other layers a layer depends upon",
180                        static_data_name="dependencies",
181                        static_data_template=deps_template)
182
183        self.add_column(title="Add | Remove",
184                        help_text="Add or remove layers to / from your project",
185                        hideable=False,
186                        filter_name="in_current_project",
187                        static_data_name="add-del-layers",
188                        static_data_template='{% include "layer_btn.html" %}')
189
190
191class MachinesTable(ToasterTable):
192    """Table of Machines in Toaster"""
193
194    def __init__(self, *args, **kwargs):
195        super(MachinesTable, self).__init__(*args, **kwargs)
196        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."
197        self.title = "Compatible machines"
198        self.default_orderby = "name"
199
200    def get_context_data(self, **kwargs):
201        context = super(MachinesTable, self).get_context_data(**kwargs)
202        context['project'] = Project.objects.get(pk=kwargs['pid'])
203        return context
204
205    def setup_filters(self, *args, **kwargs):
206        project = Project.objects.get(pk=kwargs['pid'])
207
208        in_current_project_filter = TableFilter(
209            "in_current_project",
210            "Filter by project machines"
211        )
212
213        in_project_action = TableFilterActionToggle(
214            "in_project",
215            "Machines provided by layers added to this project",
216            ProjectFilters.in_project(self.project_layers)
217        )
218
219        not_in_project_action = TableFilterActionToggle(
220            "not_in_project",
221            "Machines provided by layers not added to this project",
222            ProjectFilters.not_in_project(self.project_layers)
223        )
224
225        in_current_project_filter.add_action(in_project_action)
226        in_current_project_filter.add_action(not_in_project_action)
227        self.add_filter(in_current_project_filter)
228
229    def setup_queryset(self, *args, **kwargs):
230        prj = Project.objects.get(pk = kwargs['pid'])
231        self.queryset = prj.get_all_compatible_machines()
232        self.queryset = self.queryset.order_by(self.default_orderby)
233
234        self.static_context_extra['current_layers'] = \
235                self.project_layers = \
236                prj.get_project_layer_versions(pk=True)
237
238    def setup_columns(self, *args, **kwargs):
239
240        self.add_column(title="Machine",
241                        hideable=False,
242                        orderable=True,
243                        field_name="name")
244
245        self.add_column(title="Description",
246                        field_name="description")
247
248        layer_link_template = '''
249        <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
250        {{data.layer_version.layer.name}}</a>
251        '''
252
253        self.add_column(title="Layer",
254                        static_data_name="layer_version__layer__name",
255                        static_data_template=layer_link_template,
256                        orderable=True)
257
258        self.add_column(title="Git revision",
259                        help_text="The Git branch, tag or commit. For the layers from the OpenEmbedded layer source, the revision is always the branch compatible with the Yocto Project version you selected for this project",
260                        hidden=True,
261                        field_name="layer_version__get_vcs_reference")
262
263        machine_file_template = '''<code>conf/machine/{{data.name}}.conf</code>
264        <a href="{{data.get_vcs_machine_file_link_url}}" target="_blank"><span class="glyphicon glyphicon-new-window"></i></a>'''
265
266        self.add_column(title="Machine file",
267                        hidden=True,
268                        static_data_name="machinefile",
269                        static_data_template=machine_file_template)
270
271        self.add_column(title="Select",
272                        help_text="Sets the selected machine as the project machine. You can only have one machine per project",
273                        hideable=False,
274                        filter_name="in_current_project",
275                        static_data_name="add-del-layers",
276                        static_data_template='{% include "machine_btn.html" %}')
277
278
279class LayerMachinesTable(MachinesTable):
280    """ Smaller version of the Machines table for use in layer details """
281
282    def __init__(self, *args, **kwargs):
283        super(LayerMachinesTable, self).__init__(*args, **kwargs)
284
285    def get_context_data(self, **kwargs):
286        context = super(LayerMachinesTable, self).get_context_data(**kwargs)
287        context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
288        return context
289
290
291    def setup_queryset(self, *args, **kwargs):
292        MachinesTable.setup_queryset(self, *args, **kwargs)
293
294        self.queryset = self.queryset.filter(layer_version__pk=int(kwargs['layerid']))
295        self.queryset = self.queryset.order_by(self.default_orderby)
296        self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
297
298    def setup_columns(self, *args, **kwargs):
299        self.add_column(title="Machine",
300                        hideable=False,
301                        orderable=True,
302                        field_name="name")
303
304        self.add_column(title="Description",
305                        field_name="description")
306
307        select_btn_template = '''
308        <a href="{% url "project" extra.pid %}?setMachine={{data.name}}"
309        class="btn btn-default btn-block select-machine-btn
310        {% if extra.in_prj == 0%}disabled{%endif%}">Select machine</a>
311        '''
312
313        self.add_column(title="Select machine",
314                        static_data_name="add-del-layers",
315                        static_data_template=select_btn_template)
316
317
318class RecipesTable(ToasterTable):
319    """Table of All Recipes in Toaster"""
320
321    def __init__(self, *args, **kwargs):
322        super(RecipesTable, self).__init__(*args, **kwargs)
323        self.empty_state = "Toaster has no recipe information. To generate recipe information you need to run a build."
324
325    build_col = { 'title' : "Build",
326            'help_text' : "Before building a recipe, you might need to add the corresponding layer to your project",
327            'hideable' : False,
328            'filter_name' : "in_current_project",
329            'static_data_name' : "add-del-layers",
330            'static_data_template' : '{% include "recipe_btn.html" %}'}
331    if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
332            build_col['static_data_template'] = '{% include "recipe_add_btn.html" %}'
333
334    def get_context_data(self, **kwargs):
335        project = Project.objects.get(pk=kwargs['pid'])
336        context = super(RecipesTable, self).get_context_data(**kwargs)
337
338        context['project'] = project
339        context['projectlayers'] = [player.layercommit.id for player in ProjectLayer.objects.filter(project=context['project'])]
340
341        return context
342
343    def setup_filters(self, *args, **kwargs):
344        table_filter = TableFilter(
345            'in_current_project',
346            'Filter by project recipes'
347        )
348
349        in_project_action = TableFilterActionToggle(
350            'in_project',
351            'Recipes provided by layers added to this project',
352            ProjectFilters.in_project(self.project_layers)
353        )
354
355        not_in_project_action = TableFilterActionToggle(
356            'not_in_project',
357            'Recipes provided by layers not added to this project',
358            ProjectFilters.not_in_project(self.project_layers)
359        )
360
361        table_filter.add_action(in_project_action)
362        table_filter.add_action(not_in_project_action)
363        self.add_filter(table_filter)
364
365    def setup_queryset(self, *args, **kwargs):
366        prj = Project.objects.get(pk = kwargs['pid'])
367
368        # Project layers used by the filters
369        self.project_layers = prj.get_project_layer_versions(pk=True)
370
371        # Project layers used to switch the button states
372        self.static_context_extra['current_layers'] = self.project_layers
373
374        self.queryset = prj.get_all_compatible_recipes()
375
376
377    def setup_columns(self, *args, **kwargs):
378
379        self.add_column(title="Version",
380                        hidden=False,
381                        field_name="version")
382
383        self.add_column(title="Description",
384                        field_name="get_description_or_summary")
385
386        recipe_file_template = '''
387        <code>{{data.file_path}}</code>
388        <a href="{{data.get_vcs_recipe_file_link_url}}" target="_blank">
389          <span class="glyphicon glyphicon-new-window"></i>
390        </a>
391         '''
392
393        self.add_column(title="Recipe file",
394                        help_text="Path to the recipe .bb file",
395                        hidden=True,
396                        static_data_name="recipe-file",
397                        static_data_template=recipe_file_template)
398
399        self.add_column(title="Section",
400                        help_text="The section in which recipes should be categorized",
401                        hidden=True,
402                        orderable=True,
403                        field_name="section")
404
405        layer_link_template = '''
406        <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
407        {{data.layer_version.layer.name}}</a>
408        '''
409
410        self.add_column(title="Layer",
411                        help_text="The name of the layer providing the recipe",
412                        orderable=True,
413                        static_data_name="layer_version__layer__name",
414                        static_data_template=layer_link_template)
415
416        self.add_column(title="License",
417                        help_text="The list of source licenses for the recipe. Multiple license names separated by the pipe character indicates a choice between licenses. Multiple license names separated by the ampersand character indicates multiple licenses exist that cover different parts of the source",
418                        hidden=True,
419                        orderable=True,
420                        field_name="license")
421
422        revision_link_template = '''
423        {% if data.layer_version.layer.local_source_dir %}
424        <span class="text-muted">Not applicable</span>
425        <span class="glyphicon glyphicon-question-sign get-help" data-original-title="" title="The source code of {{data.layer_version.layer.name}} is not in a Git repository, so there is no revision associated with it"> </span>
426        {% else %}
427        {{data.layer_version.get_vcs_reference}}
428        {% endif %}
429        '''
430
431        self.add_column(title="Git revision",
432                        hidden=True,
433                        static_data_name="layer_version__get_vcs_reference",
434                        static_data_template=revision_link_template)
435
436
437class LayerRecipesTable(RecipesTable):
438    """ Smaller version of the Recipes table for use in layer details """
439
440    def __init__(self, *args, **kwargs):
441        super(LayerRecipesTable, self).__init__(*args, **kwargs)
442        self.default_orderby = "name"
443
444    def get_context_data(self, **kwargs):
445        context = super(LayerRecipesTable, self).get_context_data(**kwargs)
446        context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid'])
447        return context
448
449
450    def setup_queryset(self, *args, **kwargs):
451        self.queryset = \
452                Recipe.objects.filter(layer_version__pk=int(kwargs['layerid']))
453
454        self.queryset = self.queryset.order_by(self.default_orderby)
455        self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
456
457    def setup_columns(self, *args, **kwargs):
458        self.add_column(title="Recipe",
459                        help_text="Information about a single piece of software, including where to download the source, configuration options, how to compile the source files and how to package the compiled output",
460                        hideable=False,
461                        orderable=True,
462                        field_name="name")
463
464        self.add_column(title="Version",
465                        field_name="version")
466
467        self.add_column(title="Description",
468                        field_name="get_description_or_summary")
469
470        build_recipe_template = '''
471        <a class="btn btn-default btn-block build-recipe-btn
472        {% if extra.in_prj == 0 %}disabled{% endif %}"
473        data-recipe-name="{{data.name}}">Build recipe</a>
474        '''
475
476        self.add_column(title="Build recipe",
477                        static_data_name="add-del-layers",
478                        static_data_template=build_recipe_template)
479
480class CustomImagesTable(ToasterTable):
481    """ Table to display your custom images """
482    def __init__(self, *args, **kwargs):
483        super(CustomImagesTable, self).__init__(*args, **kwargs)
484        self.title = "Custom images"
485        self.default_orderby = "name"
486
487    def get_context_data(self, **kwargs):
488        context = super(CustomImagesTable, self).get_context_data(**kwargs)
489
490        empty_state_template = '''
491        You have not created any custom images yet.
492        <a href="{% url 'newcustomimage' data.pid %}">
493        Create your first custom image</a>
494        '''
495        context['empty_state'] = self.render_static_data(empty_state_template,
496                                                         kwargs)
497        project = Project.objects.get(pk=kwargs['pid'])
498
499        # TODO put project into the ToasterTable base class
500        context['project'] = project
501        return context
502
503    def setup_queryset(self, *args, **kwargs):
504        prj = Project.objects.get(pk = kwargs['pid'])
505        self.queryset = CustomImageRecipe.objects.filter(project=prj)
506        self.queryset = self.queryset.order_by(self.default_orderby)
507
508    def setup_columns(self, *args, **kwargs):
509
510        name_link_template = '''
511        <a href="{% url 'customrecipe' extra.pid data.id %}">
512          {{data.name}}
513        </a>
514        '''
515
516        self.add_column(title="Custom image",
517                        hideable=False,
518                        orderable=True,
519                        field_name="name",
520                        static_data_name="name",
521                        static_data_template=name_link_template)
522
523        recipe_file_template = '''
524        {% if data.get_base_recipe_file %}
525        <code>{{data.name}}_{{data.version}}.bb</code>
526        <a href="{% url 'customrecipedownload' extra.pid data.pk %}"
527        class="glyphicon glyphicon-download-alt get-help" title="Download recipe file"></a>
528        {% endif %}'''
529
530        self.add_column(title="Recipe file",
531                        static_data_name='recipe_file_download',
532                        static_data_template=recipe_file_template)
533
534        approx_packages_template = '''
535        {% if data.get_all_packages.count > 0 %}
536        <a href="{% url 'customrecipe' extra.pid data.id %}">
537          {{data.get_all_packages.count}}
538        </a>
539        {% endif %}'''
540
541        self.add_column(title="Packages",
542                        static_data_name='approx_packages',
543                        static_data_template=approx_packages_template)
544
545
546        build_btn_template = '''
547        <button data-recipe-name="{{data.name}}"
548        class="btn btn-default btn-block build-recipe-btn">
549        Build
550        </button>'''
551
552        self.add_column(title="Build",
553                        hideable=False,
554                        static_data_name='build_custom_img',
555                        static_data_template=build_btn_template)
556
557class ImageRecipesTable(RecipesTable):
558    """ A subset of the recipes table which displayed just image recipes """
559
560    def __init__(self, *args, **kwargs):
561        super(ImageRecipesTable, self).__init__(*args, **kwargs)
562        self.title = "Compatible image recipes"
563        self.default_orderby = "name"
564
565    def setup_queryset(self, *args, **kwargs):
566        super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
567
568        custom_image_recipes = CustomImageRecipe.objects.filter(
569                project=kwargs['pid'])
570        self.queryset = self.queryset.filter(
571                Q(is_image=True) & ~Q(pk__in=custom_image_recipes))
572        self.queryset = self.queryset.order_by(self.default_orderby)
573
574
575    def setup_columns(self, *args, **kwargs):
576
577        name_link_template = '''
578        <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
579        '''
580
581        self.add_column(title="Image recipe",
582                        help_text="When you build an image recipe, you get an "
583                                  "image: a root file system you can"
584                                  "deploy to a machine",
585                        hideable=False,
586                        orderable=True,
587                        static_data_name="name",
588                        static_data_template=name_link_template,
589                        field_name="name")
590
591        super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
592
593        self.add_column(**RecipesTable.build_col)
594
595
596class NewCustomImagesTable(ImageRecipesTable):
597    """ Table which displays Images recipes which can be customised """
598    def __init__(self, *args, **kwargs):
599        super(NewCustomImagesTable, self).__init__(*args, **kwargs)
600        self.title = "Select the image recipe you want to customise"
601
602    def setup_queryset(self, *args, **kwargs):
603        super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
604        prj = Project.objects.get(pk = kwargs['pid'])
605        self.static_context_extra['current_layers'] = \
606                prj.get_project_layer_versions(pk=True)
607
608        self.queryset = self.queryset.filter(is_image=True)
609
610    def setup_columns(self, *args, **kwargs):
611
612        name_link_template = '''
613        <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a>
614        '''
615
616        self.add_column(title="Image recipe",
617                        help_text="When you build an image recipe, you get an "
618                                  "image: a root file system you can"
619                                  "deploy to a machine",
620                        hideable=False,
621                        orderable=True,
622                        static_data_name="name",
623                        static_data_template=name_link_template,
624                        field_name="name")
625
626        super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
627
628        self.add_column(title="Customise",
629                        hideable=False,
630                        filter_name="in_current_project",
631                        static_data_name="customise-or-add-recipe",
632                        static_data_template='{% include "customise_btn.html" %}')
633
634
635class SoftwareRecipesTable(RecipesTable):
636    """ Displays just the software recipes """
637    def __init__(self, *args, **kwargs):
638        super(SoftwareRecipesTable, self).__init__(*args, **kwargs)
639        self.title = "Compatible software recipes"
640        self.default_orderby = "name"
641
642    def setup_queryset(self, *args, **kwargs):
643        super(SoftwareRecipesTable, self).setup_queryset(*args, **kwargs)
644
645        self.queryset = self.queryset.filter(is_image=False)
646        self.queryset = self.queryset.order_by(self.default_orderby)
647
648
649    def setup_columns(self, *args, **kwargs):
650        self.add_column(title="Software recipe",
651                        help_text="Information about a single piece of "
652                        "software, including where to download the source, "
653                        "configuration options, how to compile the source "
654                        "files and how to package the compiled output",
655                        hideable=False,
656                        orderable=True,
657                        field_name="name")
658
659        super(SoftwareRecipesTable, self).setup_columns(*args, **kwargs)
660
661        self.add_column(**RecipesTable.build_col)
662
663class PackagesTable(ToasterTable):
664    """ Table to display the packages in a recipe from it's last successful
665    build"""
666
667    def __init__(self, *args, **kwargs):
668        super(PackagesTable, self).__init__(*args, **kwargs)
669        self.title = "Packages included"
670        self.packages = None
671        self.default_orderby = "name"
672
673    def create_package_list(self, recipe, project_id):
674        """Creates a list of packages for the specified recipe by looking for
675        the last SUCCEEDED build of ther recipe"""
676
677        target = Target.objects.filter(Q(target=recipe.name) &
678                                       Q(build__project_id=project_id) &
679                                       Q(build__outcome=Build.SUCCEEDED)
680                                      ).last()
681
682        if target:
683            pkgs = target.target_installed_package_set.values_list('package',
684                                                                   flat=True)
685            return Package.objects.filter(pk__in=pkgs)
686
687        # Target/recipe never successfully built so empty queryset
688        return Package.objects.none()
689
690    def get_context_data(self, **kwargs):
691        """Context for rendering the sidebar and other items on the recipe
692        details page """
693        context = super(PackagesTable, self).get_context_data(**kwargs)
694
695        recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
696        project = Project.objects.get(pk=kwargs['pid'])
697
698        in_project = (recipe.layer_version.pk in
699                      project.get_project_layer_versions(pk=True))
700
701        packages = self.create_package_list(recipe, project.pk)
702
703        context.update({'project': project,
704                        'recipe' : recipe,
705                        'packages': packages,
706                        'approx_pkg_size' : packages.aggregate(Sum('size')),
707                        'in_project' : in_project,
708                       })
709
710        return context
711
712    def setup_queryset(self, *args, **kwargs):
713        recipe = Recipe.objects.get(pk=kwargs['recipe_id'])
714        self.static_context_extra['target_name'] = recipe.name
715
716        self.queryset = self.create_package_list(recipe, kwargs['pid'])
717        self.queryset = self.queryset.order_by('name')
718
719    def setup_columns(self, *args, **kwargs):
720        self.add_column(title="Package",
721                        hideable=False,
722                        orderable=True,
723                        field_name="name")
724
725        self.add_column(title="Package Version",
726                        field_name="version",
727                        hideable=False)
728
729        self.add_column(title="Approx Size",
730                        orderable=True,
731                        field_name="size",
732                        static_data_name="size",
733                        static_data_template="{% load projecttags %} \
734                        {{data.size|filtered_filesizeformat}}")
735
736        self.add_column(title="License",
737                        field_name="license",
738                        orderable=True,
739                        hidden=True)
740
741
742        self.add_column(title="Dependencies",
743                        static_data_name="dependencies",
744                        static_data_template='\
745                        {% include "snippets/pkg_dependencies_popover.html" %}')
746
747        self.add_column(title="Reverse dependencies",
748                        static_data_name="reverse_dependencies",
749                        static_data_template='\
750                        {% include "snippets/pkg_revdependencies_popover.html" %}',
751                        hidden=True)
752
753        self.add_column(title="Recipe",
754                        field_name="recipe__name",
755                        orderable=True,
756                        hidden=True)
757
758        self.add_column(title="Recipe version",
759                        field_name="recipe__version",
760                        hidden=True)
761
762
763class SelectPackagesTable(PackagesTable):
764    """ Table to display the packages to add and remove from an image """
765
766    def __init__(self, *args, **kwargs):
767        super(SelectPackagesTable, self).__init__(*args, **kwargs)
768        self.title = "Add | Remove packages"
769
770    def setup_queryset(self, *args, **kwargs):
771        self.cust_recipe =\
772            CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
773        prj = Project.objects.get(pk = kwargs['pid'])
774
775        current_packages = self.cust_recipe.get_all_packages()
776
777        current_recipes = prj.get_available_recipes()
778
779        # only show packages where recipes->layers are in the project
780        self.queryset = CustomImagePackage.objects.filter(
781                ~Q(recipe=None) &
782                Q(recipe__in=current_recipes))
783
784        self.queryset = self.queryset.order_by('name')
785
786        # This target is the target used to work out which group of dependences
787        # to display, if we've built the custom image we use it otherwise we
788        # can use the based recipe instead
789        if prj.build_set.filter(target__target=self.cust_recipe.name).count()\
790           > 0:
791            self.static_context_extra['target_name'] = self.cust_recipe.name
792        else:
793            self.static_context_extra['target_name'] =\
794                    Package_DependencyManager.TARGET_LATEST
795
796        self.static_context_extra['recipe_id'] = kwargs['custrecipeid']
797
798
799        self.static_context_extra['current_packages'] = \
800                current_packages.values_list('pk', flat=True)
801
802    def get_context_data(self, **kwargs):
803        # to reuse the Super class map the custrecipeid to the recipe_id
804        kwargs['recipe_id'] = kwargs['custrecipeid']
805        context = super(SelectPackagesTable, self).get_context_data(**kwargs)
806        custom_recipe = \
807            CustomImageRecipe.objects.get(pk=kwargs['custrecipeid'])
808
809        context['recipe'] = custom_recipe
810        context['approx_pkg_size'] = \
811                        custom_recipe.get_all_packages().aggregate(Sum('size'))
812        return context
813
814
815    def setup_columns(self, *args, **kwargs):
816        super(SelectPackagesTable, self).setup_columns(*args, **kwargs)
817
818        add_remove_template = '{% include "pkg_add_rm_btn.html" %}'
819
820        self.add_column(title="Add | Remove",
821                        hideable=False,
822                        help_text="Use the add and remove buttons to modify "
823                        "the package content of your custom image",
824                        static_data_name="add_rm_pkg_btn",
825                        static_data_template=add_remove_template,
826                        filter_name='in_current_image_filter')
827
828    def setup_filters(self, *args, **kwargs):
829        in_current_image_filter = TableFilter(
830            'in_current_image_filter',
831            'Filter by added packages'
832        )
833
834        in_image_action = TableFilterActionToggle(
835            'in_image',
836            'Packages in %s' % self.cust_recipe.name,
837            Q(pk__in=self.static_context_extra['current_packages'])
838        )
839
840        not_in_image_action = TableFilterActionToggle(
841            'not_in_image',
842            'Packages not added to %s' % self.cust_recipe.name,
843            ~Q(pk__in=self.static_context_extra['current_packages'])
844        )
845
846        in_current_image_filter.add_action(in_image_action)
847        in_current_image_filter.add_action(not_in_image_action)
848        self.add_filter(in_current_image_filter)
849
850class ProjectsTable(ToasterTable):
851    """Table of projects in Toaster"""
852
853    def __init__(self, *args, **kwargs):
854        super(ProjectsTable, self).__init__(*args, **kwargs)
855        self.default_orderby = '-updated'
856        self.title = 'All projects'
857        self.static_context_extra['Build'] = Build
858
859    def get_context_data(self, **kwargs):
860        return super(ProjectsTable, self).get_context_data(**kwargs)
861
862    def setup_queryset(self, *args, **kwargs):
863        queryset = Project.objects.all()
864
865        # annotate each project with its number of builds
866        queryset = queryset.annotate(num_builds=Count('build'))
867
868        # exclude the command line builds project if it has no builds
869        q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0)
870        queryset = queryset.filter(Q(is_default=False) |
871                                   q_default_with_builds)
872
873        # order rows
874        queryset = queryset.order_by(self.default_orderby)
875
876        self.queryset = queryset
877
878    # columns: last activity on (updated) - DEFAULT, project (name), release,
879    # machine, number of builds, last build outcome, recipe (name),  errors,
880    # warnings, image files
881    def setup_columns(self, *args, **kwargs):
882        name_template = '''
883        {% load project_url_tag %}
884        <span data-project-field="name">
885          <a href="{% project_url data %}">
886            {{data.name}}
887          </a>
888        </span>
889        '''
890
891        last_activity_on_template = '''
892        {% load project_url_tag %}
893        <span data-project-field="updated">
894            {{data.updated | date:"d/m/y H:i"}}
895        </span>
896        '''
897
898        release_template = '''
899        <span data-project-field="release">
900          {% if data.release %}
901            {{data.release.name}}
902          {% elif data.is_default %}
903            <span class="text-muted">Not applicable</span>
904            <span class="glyphicon glyphicon-question-sign get-help hover-help"
905               title="This project does not have a release set.
906               It simply collects information about the builds you start from
907               the command line while Toaster is running"
908               style="visibility: hidden;">
909            </span>
910          {% else %}
911            No release available
912          {% endif %}
913        </span>
914        '''
915
916        machine_template = '''
917        <span data-project-field="machine">
918          {% if data.is_default %}
919            <span class="text-muted">Not applicable</span>
920            <span class="glyphicon glyphicon-question-sign get-help hover-help"
921               title="This project does not have a machine
922               set. It simply collects information about the builds you
923               start from the command line while Toaster is running"
924               style="visibility: hidden;"></span>
925          {% else %}
926            {{data.get_current_machine_name}}
927          {% endif %}
928        </span>
929        '''
930
931        number_of_builds_template = '''
932        {% if data.get_number_of_builds > 0 %}
933          <a href="{% url 'projectbuilds' data.id %}">
934            {{data.get_number_of_builds}}
935          </a>
936        {% endif %}
937        '''
938
939        last_build_outcome_template = '''
940        {% if data.get_number_of_builds > 0 %}
941          {% if data.get_last_outcome == extra.Build.SUCCEEDED %}
942            <span class="glyphicon glyphicon-ok-circle"></span>
943          {% elif data.get_last_outcome == extra.Build.FAILED %}
944            <span class="glyphicon glyphicon-minus-sign"></span>
945          {% endif %}
946        {% endif %}
947        '''
948
949        recipe_template = '''
950        {% if data.get_number_of_builds > 0 %}
951          <a href="{% url "builddashboard" data.get_last_build_id %}">
952            {{data.get_last_target}}
953          </a>
954        {% endif %}
955        '''
956
957        errors_template = '''
958        {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %}
959          <a class="errors.count text-danger"
960             href="{% url "builddashboard" data.get_last_build_id %}#errors">
961            {{data.get_last_errors}} error{{data.get_last_errors | pluralize}}
962          </a>
963        {% endif %}
964        '''
965
966        warnings_template = '''
967        {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %}
968          <a class="warnings.count text-warning"
969             href="{% url "builddashboard" data.get_last_build_id %}#warnings">
970            {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}}
971          </a>
972        {% endif %}
973        '''
974
975        image_files_template = '''
976        {% if data.get_number_of_builds > 0 and data.get_last_outcome == extra.Build.SUCCEEDED %}
977          {{data.get_last_build_extensions}}
978        {% endif %}
979        '''
980
981        self.add_column(title='Project',
982                        hideable=False,
983                        orderable=True,
984                        static_data_name='name',
985                        static_data_template=name_template)
986
987        self.add_column(title='Last activity on',
988                        help_text='Starting date and time of the \
989                                   last project build. If the project has no \
990                                   builds, this shows the date the project was \
991                                   created.',
992                        hideable=False,
993                        orderable=True,
994                        static_data_name='updated',
995                        static_data_template=last_activity_on_template)
996
997        self.add_column(title='Release',
998                        help_text='The version of the build system used by \
999                                   the project',
1000                        hideable=False,
1001                        orderable=True,
1002                        static_data_name='release',
1003                        static_data_template=release_template)
1004
1005        self.add_column(title='Machine',
1006                        help_text='The hardware currently selected for the \
1007                                   project',
1008                        hideable=False,
1009                        orderable=False,
1010                        static_data_name='machine',
1011                        static_data_template=machine_template)
1012
1013        self.add_column(title='Builds',
1014                        help_text='The number of builds which have been run \
1015                                   for the project',
1016                        hideable=False,
1017                        orderable=False,
1018                        static_data_name='number_of_builds',
1019                        static_data_template=number_of_builds_template)
1020
1021        self.add_column(title='Last build outcome',
1022                        help_text='Indicates whether the last project build \
1023                                   completed successfully or failed',
1024                        hideable=True,
1025                        orderable=False,
1026                        static_data_name='last_build_outcome',
1027                        static_data_template=last_build_outcome_template)
1028
1029        self.add_column(title='Recipe',
1030                        help_text='The last recipe which was built in this \
1031                                   project',
1032                        hideable=True,
1033                        orderable=False,
1034                        static_data_name='recipe_name',
1035                        static_data_template=recipe_template)
1036
1037        self.add_column(title='Errors',
1038                        help_text='The number of errors encountered during \
1039                                   the last project build (if any)',
1040                        hideable=True,
1041                        orderable=False,
1042                        static_data_name='errors',
1043                        static_data_template=errors_template)
1044
1045        self.add_column(title='Warnings',
1046                        help_text='The number of warnings encountered during \
1047                                   the last project build (if any)',
1048                        hideable=True,
1049                        hidden=True,
1050                        orderable=False,
1051                        static_data_name='warnings',
1052                        static_data_template=warnings_template)
1053
1054        self.add_column(title='Image files',
1055                        help_text='The root file system types produced by \
1056                                   the last project build',
1057                        hideable=True,
1058                        hidden=True,
1059                        orderable=False,
1060                        static_data_name='image_files',
1061                        static_data_template=image_files_template)
1062
1063class BuildsTable(ToasterTable):
1064    """Table of builds in Toaster"""
1065
1066    def __init__(self, *args, **kwargs):
1067        super(BuildsTable, self).__init__(*args, **kwargs)
1068        self.default_orderby = '-completed_on'
1069        self.static_context_extra['Build'] = Build
1070        self.static_context_extra['Task'] = Task
1071
1072        # attributes that are overridden in subclasses
1073
1074        # title for the page
1075        self.title = ''
1076
1077        # 'project' or 'all'; determines how the mrb (most recent builds)
1078        # section is displayed
1079        self.mrb_type = ''
1080
1081    def get_builds(self):
1082        """
1083        overridden in ProjectBuildsTable to return builds for a
1084        single project
1085        """
1086        return Build.objects.all()
1087
1088    def get_context_data(self, **kwargs):
1089        context = super(BuildsTable, self).get_context_data(**kwargs)
1090
1091        # should be set in subclasses
1092        context['mru'] = []
1093
1094        context['mrb_type'] = self.mrb_type
1095
1096        return context
1097
1098    def setup_queryset(self, *args, **kwargs):
1099        """
1100        The queryset is annotated so that it can be sorted by number of
1101        errors and number of warnings; but note that the criteria for
1102        finding the log messages to populate these fields should match those
1103        used in the Build model (orm/models.py) to populate the errors and
1104        warnings properties
1105        """
1106        queryset = self.get_builds()
1107
1108        # Don't include in progress builds pr cancelled builds
1109        queryset = queryset.exclude(Q(outcome=Build.IN_PROGRESS) |
1110                                    Q(outcome=Build.CANCELLED))
1111
1112        # sort
1113        queryset = queryset.order_by(self.default_orderby)
1114
1115        # annotate with number of ERROR, EXCEPTION and CRITICAL log messages
1116        criteria = (Q(logmessage__level=LogMessage.ERROR) |
1117                    Q(logmessage__level=LogMessage.EXCEPTION) |
1118                    Q(logmessage__level=LogMessage.CRITICAL))
1119
1120        queryset = queryset.annotate(
1121            errors_no=Count(
1122                Case(
1123                    When(criteria, then=Value(1)),
1124                    output_field=IntegerField()
1125                )
1126            )
1127        )
1128
1129        # annotate with number of WARNING log messages
1130        queryset = queryset.annotate(
1131            warnings_no=Count(
1132                Case(
1133                    When(logmessage__level=LogMessage.WARNING, then=Value(1)),
1134                    output_field=IntegerField()
1135                )
1136            )
1137        )
1138
1139        self.queryset = queryset
1140
1141    def setup_columns(self, *args, **kwargs):
1142        outcome_template = '''
1143        {% if data.outcome == data.SUCCEEDED %}
1144            <span class="glyphicon glyphicon-ok-circle"></span>
1145        {% elif data.outcome == data.FAILED %}
1146            <span class="glyphicon glyphicon-minus-sign"></span>
1147        {% endif %}
1148
1149        {% if data.cooker_log_path %}
1150            &nbsp;
1151            <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
1152               <span class="glyphicon glyphicon-download-alt get-help"
1153               data-original-title="Download build log"></span>
1154            </a>
1155        {% endif %}
1156        '''
1157
1158        recipe_template = '''
1159        {% for target_label in data.target_labels %}
1160            <a href="{% url "builddashboard" data.id %}">
1161                {{target_label}}
1162            </a>
1163            <br />
1164        {% endfor %}
1165        '''
1166
1167        machine_template = '''
1168        {{data.machine}}
1169        '''
1170
1171        started_on_template = '''
1172        {{data.started_on | date:"d/m/y H:i"}}
1173        '''
1174
1175        completed_on_template = '''
1176        {{data.completed_on | date:"d/m/y H:i"}}
1177        '''
1178
1179        failed_tasks_template = '''
1180        {% if data.failed_tasks.count == 1 %}
1181            <a class="text-danger" href="{% url "task" data.id data.failed_tasks.0.id %}">
1182                <span>
1183                    {{data.failed_tasks.0.recipe.name}} {{data.failed_tasks.0.task_name}}
1184                </span>
1185            </a>
1186            <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}">
1187                <span class="glyphicon glyphicon-download-alt get-help"
1188                   title="Download task log file">
1189                </span>
1190            </a>
1191        {% elif data.failed_tasks.count > 1 %}
1192            <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}">
1193                <span class="text-danger">{{data.failed_tasks.count}} tasks</span>
1194            </a>
1195        {% endif %}
1196        '''
1197
1198        errors_template = '''
1199        {% if data.errors_no %}
1200            <a class="errors.count text-danger" href="{% url "builddashboard" data.id %}#errors">
1201                {{data.errors_no}} error{{data.errors_no|pluralize}}
1202            </a>
1203        {% endif %}
1204        '''
1205
1206        warnings_template = '''
1207        {% if data.warnings_no %}
1208            <a class="warnings.count text-warning" href="{% url "builddashboard" data.id %}#warnings">
1209                {{data.warnings_no}} warning{{data.warnings_no|pluralize}}
1210            </a>
1211        {% endif %}
1212        '''
1213
1214        time_template = '''
1215        {% load projecttags %}
1216        {% if data.outcome == extra.Build.SUCCEEDED %}
1217            <a href="{% url "buildtime" data.id %}">
1218                {{data.timespent_seconds | sectohms}}
1219            </a>
1220        {% else %}
1221            {{data.timespent_seconds | sectohms}}
1222        {% endif %}
1223        '''
1224
1225        image_files_template = '''
1226        {% if data.outcome == extra.Build.SUCCEEDED %}
1227            {{data.get_image_file_extensions}}
1228        {% endif %}
1229        '''
1230
1231        self.add_column(title='Outcome',
1232                        help_text='Final state of the build (successful \
1233                                   or failed)',
1234                        hideable=False,
1235                        orderable=True,
1236                        filter_name='outcome_filter',
1237                        static_data_name='outcome',
1238                        static_data_template=outcome_template)
1239
1240        self.add_column(title='Recipe',
1241                        help_text='What was built (i.e. one or more recipes \
1242                                   or image recipes)',
1243                        hideable=False,
1244                        orderable=False,
1245                        static_data_name='target',
1246                        static_data_template=recipe_template)
1247
1248        self.add_column(title='Machine',
1249                        help_text='Hardware for which you are building a \
1250                                   recipe or image recipe',
1251                        hideable=False,
1252                        orderable=True,
1253                        static_data_name='machine',
1254                        static_data_template=machine_template)
1255
1256        self.add_column(title='Started on',
1257                        help_text='The date and time when the build started',
1258                        hideable=True,
1259                        hidden=True,
1260                        orderable=True,
1261                        filter_name='started_on_filter',
1262                        static_data_name='started_on',
1263                        static_data_template=started_on_template)
1264
1265        self.add_column(title='Completed on',
1266                        help_text='The date and time when the build finished',
1267                        hideable=False,
1268                        orderable=True,
1269                        filter_name='completed_on_filter',
1270                        static_data_name='completed_on',
1271                        static_data_template=completed_on_template)
1272
1273        self.add_column(title='Failed tasks',
1274                        help_text='The number of tasks which failed during \
1275                                   the build',
1276                        hideable=True,
1277                        orderable=False,
1278                        filter_name='failed_tasks_filter',
1279                        static_data_name='failed_tasks',
1280                        static_data_template=failed_tasks_template)
1281
1282        self.add_column(title='Errors',
1283                        help_text='The number of errors encountered during \
1284                                   the build (if any)',
1285                        hideable=True,
1286                        orderable=True,
1287                        static_data_name='errors_no',
1288                        static_data_template=errors_template)
1289
1290        self.add_column(title='Warnings',
1291                        help_text='The number of warnings encountered during \
1292                                   the build (if any)',
1293                        hideable=True,
1294                        orderable=True,
1295                        static_data_name='warnings_no',
1296                        static_data_template=warnings_template)
1297
1298        self.add_column(title='Time',
1299                        help_text='How long the build took to finish',
1300                        hideable=True,
1301                        hidden=True,
1302                        orderable=False,
1303                        static_data_name='time',
1304                        static_data_template=time_template)
1305
1306        self.add_column(title='Image files',
1307                        help_text='The root file system types produced by \
1308                                   the build',
1309                        hideable=True,
1310                        orderable=False,
1311                        static_data_name='image_files',
1312                        static_data_template=image_files_template)
1313
1314    def setup_filters(self, *args, **kwargs):
1315        # outcomes
1316        outcome_filter = TableFilter(
1317            'outcome_filter',
1318            'Filter builds by outcome'
1319        )
1320
1321        successful_builds_action = TableFilterActionToggle(
1322            'successful_builds',
1323            'Successful builds',
1324            Q(outcome=Build.SUCCEEDED)
1325        )
1326
1327        failed_builds_action = TableFilterActionToggle(
1328            'failed_builds',
1329            'Failed builds',
1330            Q(outcome=Build.FAILED)
1331        )
1332
1333        outcome_filter.add_action(successful_builds_action)
1334        outcome_filter.add_action(failed_builds_action)
1335        self.add_filter(outcome_filter)
1336
1337        # started on
1338        started_on_filter = TableFilter(
1339            'started_on_filter',
1340            'Filter by date when build was started'
1341        )
1342
1343        started_today_action = TableFilterActionDay(
1344            'today',
1345            'Today\'s builds',
1346            'started_on',
1347            'today'
1348        )
1349
1350        started_yesterday_action = TableFilterActionDay(
1351            'yesterday',
1352            'Yesterday\'s builds',
1353            'started_on',
1354            'yesterday'
1355        )
1356
1357        by_started_date_range_action = TableFilterActionDateRange(
1358            'date_range',
1359            'Build date range',
1360            'started_on'
1361        )
1362
1363        started_on_filter.add_action(started_today_action)
1364        started_on_filter.add_action(started_yesterday_action)
1365        started_on_filter.add_action(by_started_date_range_action)
1366        self.add_filter(started_on_filter)
1367
1368        # completed on
1369        completed_on_filter = TableFilter(
1370            'completed_on_filter',
1371            'Filter by date when build was completed'
1372        )
1373
1374        completed_today_action = TableFilterActionDay(
1375            'today',
1376            'Today\'s builds',
1377            'completed_on',
1378            'today'
1379        )
1380
1381        completed_yesterday_action = TableFilterActionDay(
1382            'yesterday',
1383            'Yesterday\'s builds',
1384            'completed_on',
1385            'yesterday'
1386        )
1387
1388        by_completed_date_range_action = TableFilterActionDateRange(
1389            'date_range',
1390            'Build date range',
1391            'completed_on'
1392        )
1393
1394        completed_on_filter.add_action(completed_today_action)
1395        completed_on_filter.add_action(completed_yesterday_action)
1396        completed_on_filter.add_action(by_completed_date_range_action)
1397        self.add_filter(completed_on_filter)
1398
1399        # failed tasks
1400        failed_tasks_filter = TableFilter(
1401            'failed_tasks_filter',
1402            'Filter builds by failed tasks'
1403        )
1404
1405        criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
1406
1407        with_failed_tasks_action = TableFilterActionToggle(
1408            'with_failed_tasks',
1409            'Builds with failed tasks',
1410            criteria
1411        )
1412
1413        without_failed_tasks_action = TableFilterActionToggle(
1414            'without_failed_tasks',
1415            'Builds without failed tasks',
1416            ~criteria
1417        )
1418
1419        failed_tasks_filter.add_action(with_failed_tasks_action)
1420        failed_tasks_filter.add_action(without_failed_tasks_action)
1421        self.add_filter(failed_tasks_filter)
1422
1423
1424class AllBuildsTable(BuildsTable):
1425    """ Builds page for all builds """
1426
1427    def __init__(self, *args, **kwargs):
1428        super(AllBuildsTable, self).__init__(*args, **kwargs)
1429        self.title = 'All builds'
1430        self.mrb_type = 'all'
1431
1432    def setup_columns(self, *args, **kwargs):
1433        """
1434        All builds page shows a column for the project
1435        """
1436
1437        super(AllBuildsTable, self).setup_columns(*args, **kwargs)
1438
1439        project_template = '''
1440        {% load project_url_tag %}
1441        <a href="{% project_url data.project %}">
1442            {{data.project.name}}
1443        </a>
1444        {% if data.project.is_default %}
1445            <span class="glyphicon glyphicon-question-sign get-help hover-help" title=""
1446               data-original-title="This project shows information about
1447               the builds you start from the command line while Toaster is
1448               running" style="visibility: hidden;"></span>
1449        {% endif %}
1450        '''
1451
1452        self.add_column(title='Project',
1453                        hideable=True,
1454                        orderable=True,
1455                        static_data_name='project',
1456                        static_data_template=project_template)
1457
1458    def get_context_data(self, **kwargs):
1459        """ Get all builds for the recent builds area """
1460        context = super(AllBuildsTable, self).get_context_data(**kwargs)
1461        context['mru'] = Build.get_recent()
1462        return context
1463
1464class ProjectBuildsTable(BuildsTable):
1465    """
1466    Builds page for a single project; a BuildsTable, with the queryset
1467    filtered by project
1468    """
1469
1470    def __init__(self, *args, **kwargs):
1471        super(ProjectBuildsTable, self).__init__(*args, **kwargs)
1472        self.title = 'All project builds'
1473        self.mrb_type = 'project'
1474
1475        # set from the querystring
1476        self.project_id = None
1477
1478    def setup_columns(self, *args, **kwargs):
1479        """
1480        Project builds table doesn't show the machines column by default
1481        """
1482
1483        super(ProjectBuildsTable, self).setup_columns(*args, **kwargs)
1484
1485        # hide the machine column
1486        self.set_column_hidden('Machine', True)
1487
1488        # allow the machine column to be hidden by the user
1489        self.set_column_hideable('Machine', True)
1490
1491    def setup_queryset(self, *args, **kwargs):
1492        """
1493        NOTE: self.project_id must be set before calling super(),
1494        as it's used in setup_queryset()
1495        """
1496        self.project_id = kwargs['pid']
1497        super(ProjectBuildsTable, self).setup_queryset(*args, **kwargs)
1498        project = Project.objects.get(pk=self.project_id)
1499        self.queryset = self.queryset.filter(project=project)
1500
1501    def get_context_data(self, **kwargs):
1502        """
1503        Get recent builds for this project, and the project itself
1504
1505        NOTE: self.project_id must be set before calling super(),
1506        as it's used in get_context_data()
1507        """
1508        self.project_id = kwargs['pid']
1509        context = super(ProjectBuildsTable, self).get_context_data(**kwargs)
1510
1511        empty_state_template = '''
1512        This project has no builds.
1513        <a href="{% url 'projectimagerecipes' data.pid %}">
1514        Choose a recipe to build</a>
1515        '''
1516        context['empty_state'] = self.render_static_data(empty_state_template,
1517                                                         kwargs)
1518
1519        project = Project.objects.get(pk=self.project_id)
1520        context['mru'] = Build.get_recent(project)
1521        context['project'] = project
1522
1523        self.setup_queryset(**kwargs)
1524        if self.queryset.count() == 0 and \
1525           project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0:
1526            context['build_in_progress_none_completed'] = True
1527        else:
1528            context['build_in_progress_none_completed'] = False
1529
1530        return context
1531
1532
1533class DistrosTable(ToasterTable):
1534    """Table of Distros in Toaster"""
1535
1536    def __init__(self, *args, **kwargs):
1537        super(DistrosTable, self).__init__(*args, **kwargs)
1538        self.empty_state = "Toaster has no distro information for this project. Sadly, 			   distro information cannot be obtained from builds, so this 				  page will remain empty."
1539        self.title = "Compatible Distros"
1540        self.default_orderby = "name"
1541
1542    def get_context_data(self, **kwargs):
1543        context = super(DistrosTable, self).get_context_data(**kwargs)
1544        context['project'] = Project.objects.get(pk=kwargs['pid'])
1545        return context
1546
1547    def setup_filters(self, *args, **kwargs):
1548        project = Project.objects.get(pk=kwargs['pid'])
1549
1550        in_current_project_filter = TableFilter(
1551            "in_current_project",
1552            "Filter by project Distros"
1553        )
1554
1555        in_project_action = TableFilterActionToggle(
1556            "in_project",
1557            "Distro provided by layers added to this project",
1558            ProjectFilters.in_project(self.project_layers)
1559        )
1560
1561        not_in_project_action = TableFilterActionToggle(
1562            "not_in_project",
1563            "Distros provided by layers not added to this project",
1564            ProjectFilters.not_in_project(self.project_layers)
1565        )
1566
1567        in_current_project_filter.add_action(in_project_action)
1568        in_current_project_filter.add_action(not_in_project_action)
1569        self.add_filter(in_current_project_filter)
1570
1571    def setup_queryset(self, *args, **kwargs):
1572        prj = Project.objects.get(pk = kwargs['pid'])
1573        self.queryset = prj.get_all_compatible_distros()
1574        self.queryset = self.queryset.order_by(self.default_orderby)
1575
1576        self.static_context_extra['current_layers'] = \
1577                self.project_layers = \
1578                prj.get_project_layer_versions(pk=True)
1579
1580    def setup_columns(self, *args, **kwargs):
1581
1582        self.add_column(title="Distro",
1583                        hideable=False,
1584                        orderable=True,
1585                        field_name="name")
1586
1587        self.add_column(title="Description",
1588                        field_name="description")
1589
1590        layer_link_template = '''
1591        <a href="{% url 'layerdetails' extra.pid data.layer_version.id %}">
1592        {{data.layer_version.layer.name}}</a>
1593        '''
1594
1595        self.add_column(title="Layer",
1596                        static_data_name="layer_version__layer__name",
1597                        static_data_template=layer_link_template,
1598                        orderable=True)
1599
1600        self.add_column(title="Git revision",
1601                        help_text="The Git branch, tag or commit. For the layers from the OpenEmbedded layer source, the revision is always the branch compatible with the Yocto Project version you selected for this project",
1602                        hidden=True,
1603                        field_name="layer_version__get_vcs_reference")
1604
1605        distro_file_template = '''<code>conf/distro/{{data.name}}.conf</code>
1606        {% if 'None' not in data.get_vcs_distro_file_link_url %}<a href="{{data.get_vcs_distro_file_link_url}}" target="_blank"><span class="glyphicon glyphicon-new-window"></i></a>{% endif %}'''
1607        self.add_column(title="Distro file",
1608                        hidden=True,
1609                        static_data_name="templatefile",
1610                        static_data_template=distro_file_template)
1611
1612        self.add_column(title="Select",
1613                        help_text="Sets the selected distro to the project",
1614                        hideable=False,
1615                        filter_name="in_current_project",
1616                        static_data_name="add-del-layers",
1617                        static_data_template='{% include "distro_btn.html" %}')
1618
1619