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