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