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