1#
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
1632