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