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