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