diff options
Diffstat (limited to 'yocto-poky/bitbake/lib/toaster/toastergui/tables.py')
-rw-r--r-- | yocto-poky/bitbake/lib/toaster/toastergui/tables.py | 1063 |
1 files changed, 969 insertions, 94 deletions
diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/tables.py b/yocto-poky/bitbake/lib/toaster/toastergui/tables.py index 9c9cda4e9..2cc2f4eb7 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/tables.py +++ b/yocto-poky/bitbake/lib/toaster/toastergui/tables.py @@ -21,29 +21,27 @@ from toastergui.widgets import ToasterTable from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project -from orm.models import CustomImageRecipe, Package -from django.db.models import Q, Max +from orm.models import CustomImageRecipe, Package, Target, Build, LogMessage, Task +from orm.models import CustomImagePackage +from django.db.models import Q, Max, Sum, Count, When, Case, Value, IntegerField from django.conf.urls import url -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, resolve +from django.http import HttpResponse from django.views.generic import TemplateView +from toastergui.tablefilter import TableFilter +from toastergui.tablefilter import TableFilterActionToggle +from toastergui.tablefilter import TableFilterActionDateRange +from toastergui.tablefilter import TableFilterActionDay -class ProjectFiltersMixin(object): - """Common mixin for recipe, machine in project filters""" +class ProjectFilters(object): + @staticmethod + def in_project(project_layers): + return Q(layer_version__in=project_layers) - def filter_in_project(self, count_only=False): - query = self.queryset.filter(layer_version__in=self.project_layers) - if count_only: - return query.count() - - self.queryset = query - - def filter_not_in_project(self, count_only=False): - query = self.queryset.exclude(layer_version__in=self.project_layers) - if count_only: - return query.count() - - self.queryset = query + @staticmethod + def not_in_project(project_layers): + return ~(ProjectFilters.in_project(project_layers)) class LayersTable(ToasterTable): """Table of layers in Toaster""" @@ -61,33 +59,32 @@ class LayersTable(ToasterTable): return context - def setup_filters(self, *args, **kwargs): project = Project.objects.get(pk=kwargs['pid']) self.project_layers = ProjectLayer.objects.filter(project=project) + in_current_project_filter = TableFilter( + "in_current_project", + "Filter by project layers" + ) - self.add_filter(title="Filter by project layers", - name="in_current_project", - filter_actions=[ - self.make_filter_action("in_project", "Layers added to this project", self.filter_in_project), - self.make_filter_action("not_in_project", "Layers not added to this project", self.filter_not_in_project) - ]) - - def filter_in_project(self, count_only=False): - query = self.queryset.filter(projectlayer__in=self.project_layers) - if count_only: - return query.count() + criteria = Q(projectlayer__in=self.project_layers) - self.queryset = query + in_project_action = TableFilterActionToggle( + "in_project", + "Layers added to this project", + criteria + ) - def filter_not_in_project(self, count_only=False): - query = self.queryset.exclude(projectlayer__in=self.project_layers) - if count_only: - return query.count() - - self.queryset = query + not_in_project_action = TableFilterActionToggle( + "not_in_project", + "Layers not added to this project", + ~criteria + ) + in_current_project_filter.add_action(in_project_action) + in_current_project_filter.add_action(not_in_project_action) + self.add_filter(in_current_project_filter) def setup_queryset(self, *args, **kwargs): prj = Project.objects.get(pk = kwargs['pid']) @@ -161,7 +158,7 @@ class LayersTable(ToasterTable): {% endwith %} ''' - self.add_column(title="Revision", + self.add_column(title="Git revision", 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", static_data_name="revision", static_data_template=revision_template) @@ -186,8 +183,8 @@ class LayersTable(ToasterTable): static_data_name="dependencies", static_data_template=deps_template) - self.add_column(title="Add | Delete", - help_text="Add or delete layers to / from your project", + self.add_column(title="Add | Remove", + help_text="Add or remove layers to / from your project", hideable=False, filter_name="in_current_project", static_data_name="add-del-layers", @@ -205,7 +202,7 @@ class LayersTable(ToasterTable): computation = lambda x: x.layer.name) -class MachinesTable(ToasterTable, ProjectFiltersMixin): +class MachinesTable(ToasterTable): """Table of Machines in Toaster""" def __init__(self, *args, **kwargs): @@ -217,25 +214,41 @@ class MachinesTable(ToasterTable, ProjectFiltersMixin): def get_context_data(self, **kwargs): context = super(MachinesTable, self).get_context_data(**kwargs) context['project'] = Project.objects.get(pk=kwargs['pid']) - context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project'])) return context def setup_filters(self, *args, **kwargs): project = Project.objects.get(pk=kwargs['pid']) - self.project_layers = project.get_project_layer_versions() - self.add_filter(title="Filter by project machines", - name="in_current_project", - filter_actions=[ - self.make_filter_action("in_project", "Machines provided by layers added to this project", self.filter_in_project), - self.make_filter_action("not_in_project", "Machines provided by layers not added to this project", self.filter_not_in_project) - ]) + in_current_project_filter = TableFilter( + "in_current_project", + "Filter by project machines" + ) + + in_project_action = TableFilterActionToggle( + "in_project", + "Machines provided by layers added to this project", + ProjectFilters.in_project(self.project_layers) + ) + + not_in_project_action = TableFilterActionToggle( + "not_in_project", + "Machines provided by layers not added to this project", + ProjectFilters.not_in_project(self.project_layers) + ) + + in_current_project_filter.add_action(in_project_action) + in_current_project_filter.add_action(not_in_project_action) + self.add_filter(in_current_project_filter) def setup_queryset(self, *args, **kwargs): prj = Project.objects.get(pk = kwargs['pid']) self.queryset = prj.get_all_compatible_machines() self.queryset = self.queryset.order_by(self.default_orderby) + self.static_context_extra['current_layers'] = \ + self.project_layers = \ + prj.get_project_layer_versions(pk=True) + def setup_columns(self, *args, **kwargs): self.add_column(title="Machine", @@ -256,7 +269,7 @@ class MachinesTable(ToasterTable, ProjectFiltersMixin): static_data_template=layer_link_template, orderable=True) - self.add_column(title="Revision", + self.add_column(title="Git revision", 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", hidden=True, field_name="layer_version__get_vcs_reference") @@ -293,6 +306,7 @@ class LayerMachinesTable(MachinesTable): MachinesTable.setup_queryset(self, *args, **kwargs) self.queryset = self.queryset.filter(layer_version__pk=int(kwargs['layerid'])) + self.queryset = self.queryset.order_by(self.default_orderby) self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count() def setup_columns(self, *args, **kwargs): @@ -311,13 +325,12 @@ class LayerMachinesTable(MachinesTable): static_data_template=select_btn_template) -class RecipesTable(ToasterTable, ProjectFiltersMixin): +class RecipesTable(ToasterTable): """Table of All Recipes in Toaster""" def __init__(self, *args, **kwargs): super(RecipesTable, self).__init__(*args, **kwargs) self.empty_state = "Toaster has no recipe information. To generate recipe information you can configure a layer source then run a build." - self.default_orderby = "name" build_col = { 'title' : "Build", 'help_text' : "Add or delete recipes to and from your project", @@ -337,12 +350,26 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin): return context def setup_filters(self, *args, **kwargs): - self.add_filter(title="Filter by project recipes", - name="in_current_project", - filter_actions=[ - self.make_filter_action("in_project", "Recipes provided by layers added to this project", self.filter_in_project), - self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", self.filter_not_in_project) - ]) + table_filter = TableFilter( + 'in_current_project', + 'Filter by project recipes' + ) + + in_project_action = TableFilterActionToggle( + 'in_project', + 'Recipes provided by layers added to this project', + ProjectFilters.in_project(self.project_layers) + ) + + not_in_project_action = TableFilterActionToggle( + 'not_in_project', + 'Recipes provided by layers not added to this project', + ProjectFilters.not_in_project(self.project_layers) + ) + + table_filter.add_action(in_project_action) + table_filter.add_action(not_in_project_action) + self.add_filter(table_filter) def setup_queryset(self, *args, **kwargs): prj = Project.objects.get(pk = kwargs['pid']) @@ -354,7 +381,6 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin): self.static_context_extra['current_layers'] = self.project_layers self.queryset = prj.get_all_compatible_recipes() - self.queryset = self.queryset.order_by(self.default_orderby) def setup_columns(self, *args, **kwargs): @@ -402,7 +428,7 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin): orderable=True, field_name="license") - self.add_column(title="Revision", + self.add_column(title="Git revision", hidden=True, field_name="layer_version__get_vcs_reference") @@ -412,6 +438,7 @@ class LayerRecipesTable(RecipesTable): def __init__(self, *args, **kwargs): super(LayerRecipesTable, self).__init__(*args, **kwargs) + self.default_orderby = "name" def get_context_data(self, **kwargs): context = super(LayerRecipesTable, self).get_context_data(**kwargs) @@ -423,6 +450,7 @@ class LayerRecipesTable(RecipesTable): self.queryset = \ Recipe.objects.filter(layer_version__pk=int(kwargs['layerid'])) + self.queryset = self.queryset.order_by(self.default_orderby) self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count() def setup_columns(self, *args, **kwargs): @@ -449,18 +477,19 @@ class CustomImagesTable(ToasterTable): def __init__(self, *args, **kwargs): super(CustomImagesTable, self).__init__(*args, **kwargs) self.title = "Custom images" + self.default_orderby = "name" def get_context_data(self, **kwargs): context = super(CustomImagesTable, self).get_context_data(**kwargs) project = Project.objects.get(pk=kwargs['pid']) + # TODO put project into the ToasterTable base class context['project'] = project - context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project'])) return context def setup_queryset(self, *args, **kwargs): prj = Project.objects.get(pk = kwargs['pid']) self.queryset = CustomImageRecipe.objects.filter(project=prj) - self.queryset = self.queryset.order_by('name') + self.queryset = self.queryset.order_by(self.default_orderby) def setup_columns(self, *args, **kwargs): @@ -472,22 +501,37 @@ class CustomImagesTable(ToasterTable): self.add_column(title="Custom image", hideable=False, + orderable=True, + field_name="name", static_data_name="name", static_data_template=name_link_template) + recipe_file_template = ''' + <code>{{data.name}}_{{data.version}}.bb</code> + <a href="{% url 'customrecipedownload' extra.pid data.pk %}"> + <i class="icon-download-alt" data-original-title="Download recipe + file"></i> + </a>''' + self.add_column(title="Recipe file", - static_data_name='recipe_file', - static_data_template='') + static_data_name='recipe_file_download', + static_data_template=recipe_file_template) + + approx_packages_template = ''' + <a href="{% url 'customrecipe' extra.pid data.id %}"> + {{data.get_all_packages.count}} + </a>''' - approx_packages_template = '<a href="#imagedetails">{{data.packages.all|length}}</a>' self.add_column(title="Approx packages", static_data_name='approx_packages', static_data_template=approx_packages_template) - build_btn_template = '''<button data-recipe-name="{{data.name}}" + build_btn_template = ''' + <button data-recipe-name="{{data.name}}" class="btn btn-block build-recipe-btn" style="margin-top: 5px;" > - Build</button>''' + Build + </button>''' self.add_column(title="Build", hideable=False, @@ -500,20 +544,32 @@ class ImageRecipesTable(RecipesTable): def __init__(self, *args, **kwargs): super(ImageRecipesTable, self).__init__(*args, **kwargs) self.title = "Compatible image recipes" + self.default_orderby = "name" def setup_queryset(self, *args, **kwargs): super(ImageRecipesTable, self).setup_queryset(*args, **kwargs) - self.queryset = self.queryset.filter(is_image=True) + custom_image_recipes = CustomImageRecipe.objects.filter( + project=kwargs['pid']) + self.queryset = self.queryset.filter( + Q(is_image=True) & ~Q(pk__in=custom_image_recipes)) + self.queryset = self.queryset.order_by(self.default_orderby) def setup_columns(self, *args, **kwargs): + + name_link_template = ''' + <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a> + ''' + self.add_column(title="Image recipe", help_text="When you build an image recipe, you get an " "image: a root file system you can" "deploy to a machine", hideable=False, orderable=True, + static_data_name="name", + static_data_template=name_link_template, field_name="name") super(ImageRecipesTable, self).setup_columns(*args, **kwargs) @@ -529,17 +585,27 @@ class NewCustomImagesTable(ImageRecipesTable): def setup_queryset(self, *args, **kwargs): super(ImageRecipesTable, self).setup_queryset(*args, **kwargs) + prj = Project.objects.get(pk = kwargs['pid']) + self.static_context_extra['current_layers'] = \ + prj.get_project_layer_versions(pk=True) self.queryset = self.queryset.filter(is_image=True) def setup_columns(self, *args, **kwargs): + + name_link_template = ''' + <a href="{% url 'recipedetails' extra.pid data.pk %}">{{data.name}}</a> + ''' + self.add_column(title="Image recipe", help_text="When you build an image recipe, you get an " "image: a root file system you can" "deploy to a machine", hideable=False, orderable=True, - field_name="recipe__name") + static_data_name="name", + static_data_template=name_link_template, + field_name="name") super(ImageRecipesTable, self).setup_columns(*args, **kwargs) @@ -555,11 +621,13 @@ class SoftwareRecipesTable(RecipesTable): def __init__(self, *args, **kwargs): super(SoftwareRecipesTable, self).__init__(*args, **kwargs) self.title = "Compatible software recipes" + self.default_orderby = "name" def setup_queryset(self, *args, **kwargs): super(SoftwareRecipesTable, self).setup_queryset(*args, **kwargs) self.queryset = self.queryset.filter(is_image=False) + self.queryset = self.queryset.order_by(self.default_orderby) def setup_columns(self, *args, **kwargs): @@ -576,33 +644,60 @@ class SoftwareRecipesTable(RecipesTable): self.add_column(**RecipesTable.build_col) - -class SelectPackagesTable(ToasterTable): - """ Table to display the packages to add and remove from an image """ +class PackagesTable(ToasterTable): + """ Table to display the packages in a recipe from it's last successful + build""" def __init__(self, *args, **kwargs): - super(SelectPackagesTable, self).__init__(*args, **kwargs) - self.title = "Add | Remove packages" + super(PackagesTable, self).__init__(*args, **kwargs) + self.title = "Packages included" + self.packages = None + self.default_orderby = "name" - def setup_queryset(self, *args, **kwargs): - cust_recipe = CustomImageRecipe.objects.get(pk=kwargs['recipeid']) - prj = Project.objects.get(pk = kwargs['pid']) + def create_package_list(self, recipe, project_id): + """Creates a list of packages for the specified recipe by looking for + the last SUCCEEDED build of ther recipe""" - current_packages = cust_recipe.packages.all() + target = Target.objects.filter(Q(target=recipe.name) & + Q(build__project_id=project_id) & + Q(build__outcome=Build.SUCCEEDED) + ).last() - # Get all the packages that are in the custom image - # Get all the packages built by builds in the current project - # but not those ones that are already in the custom image - self.queryset = Package.objects.filter( - Q(pk__in=current_packages) | - (Q(build__project=prj) & - ~Q(name__in=current_packages.values_list('name')))) + if target: + pkgs = target.target_installed_package_set.values_list('package', + flat=True) + return Package.objects.filter(pk__in=pkgs) - self.queryset = self.queryset.order_by('name') + # Target/recipe never successfully built so empty queryset + return Package.objects.none() - self.static_context_extra['recipe_id'] = kwargs['recipeid'] - self.static_context_extra['current_packages'] = \ - cust_recipe.packages.values_list('pk', flat=True) + def get_context_data(self, **kwargs): + """Context for rendering the sidebar and other items on the recipe + details page """ + context = super(PackagesTable, self).get_context_data(**kwargs) + + recipe = Recipe.objects.get(pk=kwargs['recipe_id']) + project = Project.objects.get(pk=kwargs['pid']) + + in_project = (recipe.layer_version.pk in + project.get_project_layer_versions(pk=True)) + + packages = self.create_package_list(recipe, project.pk) + + context.update({'project': project, + 'recipe' : recipe, + 'packages': packages, + 'approx_pkg_size' : packages.aggregate(Sum('size')), + 'in_project' : in_project, + }) + + return context + + def setup_queryset(self, *args, **kwargs): + recipe = Recipe.objects.get(pk=kwargs['recipe_id']) + + self.queryset = self.create_package_list(recipe, kwargs['pid']) + self.queryset = self.queryset.order_by('name') def setup_columns(self, *args, **kwargs): self.add_column(title="Package", @@ -611,18 +706,798 @@ class SelectPackagesTable(ToasterTable): field_name="name") self.add_column(title="Package Version", - field_name="version") + field_name="version", + hideable=False) self.add_column(title="Approx Size", orderable=True, static_data_name="size", static_data_template="{% load projecttags %} \ {{data.size|filtered_filesizeformat}}") - self.add_column(title="summary", - field_name="summary") + + self.add_column(title="License", + field_name="license", + orderable=True) + + + self.add_column(title="Dependencies", + static_data_name="dependencies", + static_data_template='\ + {% include "snippets/pkg_dependencies_popover.html" %}') + + self.add_column(title="Reverse dependencies", + static_data_name="reverse_dependencies", + static_data_template='\ + {% include "snippets/pkg_revdependencies_popover.html" %}', + hidden=True) + + self.add_column(title="Recipe", + field_name="recipe__name", + orderable=True, + hidden=True) + + self.add_column(title="Recipe version", + field_name="recipe__version", + hidden=True) + + +class SelectPackagesTable(PackagesTable): + """ Table to display the packages to add and remove from an image """ + + def __init__(self, *args, **kwargs): + super(SelectPackagesTable, self).__init__(*args, **kwargs) + self.title = "Add | Remove packages" + + def setup_queryset(self, *args, **kwargs): + self.cust_recipe =\ + CustomImageRecipe.objects.get(pk=kwargs['custrecipeid']) + prj = Project.objects.get(pk = kwargs['pid']) + + current_packages = self.cust_recipe.get_all_packages() + + current_recipes = prj.get_available_recipes() + + # only show packages where recipes->layers are in the project + self.queryset = CustomImagePackage.objects.filter( + ~Q(recipe=None) & + Q(recipe__in=current_recipes)) + + self.queryset = self.queryset.order_by('name') + + self.static_context_extra['recipe_id'] = kwargs['custrecipeid'] + self.static_context_extra['current_packages'] = \ + current_packages.values_list('pk', flat=True) + + def get_context_data(self, **kwargs): + # to reuse the Super class map the custrecipeid to the recipe_id + kwargs['recipe_id'] = kwargs['custrecipeid'] + context = super(SelectPackagesTable, self).get_context_data(**kwargs) + custom_recipe = \ + CustomImageRecipe.objects.get(pk=kwargs['custrecipeid']) + + context['recipe'] = custom_recipe + context['approx_pkg_size'] = \ + custom_recipe.get_all_packages().aggregate(Sum('size')) + return context + + + def setup_columns(self, *args, **kwargs): + super(SelectPackagesTable, self).setup_columns(*args, **kwargs) + + add_remove_template = '{% include "pkg_add_rm_btn.html" %}' self.add_column(title="Add | Remove", + hideable=False, help_text="Use the add and remove buttons to modify " - "the package content of you custom image", + "the package content of your custom image", static_data_name="add_rm_pkg_btn", - static_data_template='{% include "pkg_add_rm_btn.html" %}') + static_data_template=add_remove_template, + filter_name='in_current_image_filter') + + def setup_filters(self, *args, **kwargs): + in_current_image_filter = TableFilter( + 'in_current_image_filter', + 'Filter by added packages' + ) + + in_image_action = TableFilterActionToggle( + 'in_image', + 'Packages in %s' % self.cust_recipe.name, + Q(pk__in=self.static_context_extra['current_packages']) + ) + + not_in_image_action = TableFilterActionToggle( + 'not_in_image', + 'Packages not added to %s' % self.cust_recipe.name, + ~Q(pk__in=self.static_context_extra['current_packages']) + ) + + in_current_image_filter.add_action(in_image_action) + in_current_image_filter.add_action(not_in_image_action) + self.add_filter(in_current_image_filter) + +class ProjectsTable(ToasterTable): + """Table of projects in Toaster""" + + def __init__(self, *args, **kwargs): + super(ProjectsTable, self).__init__(*args, **kwargs) + self.default_orderby = '-updated' + self.title = 'All projects' + self.static_context_extra['Build'] = Build + + def get_context_data(self, **kwargs): + return super(ProjectsTable, self).get_context_data(**kwargs) + + def setup_queryset(self, *args, **kwargs): + queryset = Project.objects.all() + + # annotate each project with its number of builds + queryset = queryset.annotate(num_builds=Count('build')) + + # exclude the command line builds project if it has no builds + q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0) + queryset = queryset.filter(Q(is_default=False) | + q_default_with_builds) + + # order rows + queryset = queryset.order_by(self.default_orderby) + + self.queryset = queryset + + # columns: last activity on (updated) - DEFAULT, project (name), release, + # machine, number of builds, last build outcome, recipe (name), errors, + # warnings, image files + def setup_columns(self, *args, **kwargs): + name_template = ''' + {% load project_url_tag %} + <span data-project-field="name"> + <a href="{% project_url data %}"> + {{data.name}} + </a> + </span> + ''' + + last_activity_on_template = ''' + {% load project_url_tag %} + <span data-project-field="updated"> + <a href="{% project_url data %}"> + {{data.updated | date:"d/m/y H:i"}} + </a> + </span> + ''' + + release_template = ''' + <span data-project-field="release"> + {% if data.release %} + <a href="{% url 'project' data.id %}#project-details"> + {{data.release.name}} + </a> + {% elif data.is_default %} + <span class="muted">Not applicable</span> + <i class="icon-question-sign get-help hover-help" + data-original-title="This project does not have a release set. + It simply collects information about the builds you start from + the command line while Toaster is running" + style="visibility: hidden;"> + </i> + {% else %} + No release available + {% endif %} + </span> + ''' + + machine_template = ''' + <span data-project-field="machine"> + {% if data.is_default %} + <span class="muted">Not applicable</span> + <i class="icon-question-sign get-help hover-help" + data-original-title="This project does not have a machine + set. It simply collects information about the builds you + start from the command line while Toaster is running" + style="visibility: hidden;"></i> + {% else %} + <a href="{% url 'project' data.id %}#machine-distro"> + {{data.get_current_machine_name}} + </a> + {% endif %} + </span> + ''' + + number_of_builds_template = ''' + {% if data.get_number_of_builds > 0 %} + <a href="{% url 'projectbuilds' data.id %}"> + {{data.get_number_of_builds}} + </a> + {% else %} + <span class="muted">0</span> + {% endif %} + ''' + + last_build_outcome_template = ''' + {% if data.get_number_of_builds > 0 %} + <a href="{% url 'builddashboard' data.get_last_build_id %}"> + {% if data.get_last_outcome == extra.Build.SUCCEEDED %} + <i class="icon-ok-sign success"></i> + {% elif data.get_last_outcome == extra.Build.FAILED %} + <i class="icon-minus-sign error"></i> + {% endif %} + </a> + {% endif %} + ''' + + recipe_template = ''' + {% if data.get_number_of_builds > 0 %} + <a href="{% url "builddashboard" data.get_last_build_id %}"> + {{data.get_last_target}} + </a> + {% endif %} + ''' + + errors_template = ''' + {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %} + <a class="errors.count error" + href="{% url "builddashboard" data.get_last_build_id %}#errors"> + {{data.get_last_errors}} error{{data.get_last_errors | pluralize}} + </a> + {% endif %} + ''' + + warnings_template = ''' + {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %} + <a class="warnings.count warning" + href="{% url "builddashboard" data.get_last_build_id %}#warnings"> + {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}} + </a> + {% endif %} + ''' + + image_files_template = ''' + {% if data.get_number_of_builds > 0 and data.get_last_outcome == extra.Build.SUCCEEDED %} + <a href="{% url "builddashboard" data.get_last_build_id %}#images"> + {{data.get_last_build_extensions}} + </a> + {% endif %} + ''' + + self.add_column(title='Project', + hideable=False, + orderable=True, + static_data_name='name', + static_data_template=name_template) + + self.add_column(title='Last activity on', + help_text='Starting date and time of the \ + last project build. If the project has no \ + builds, this shows the date the project was \ + created.', + hideable=False, + orderable=True, + static_data_name='updated', + static_data_template=last_activity_on_template) + + self.add_column(title='Release', + help_text='The version of the build system used by \ + the project', + hideable=False, + orderable=True, + static_data_name='release', + static_data_template=release_template) + + self.add_column(title='Machine', + help_text='The hardware currently selected for the \ + project', + hideable=False, + orderable=False, + static_data_name='machine', + static_data_template=machine_template) + + self.add_column(title='Number of builds', + help_text='The number of builds which have been run \ + for the project', + hideable=False, + orderable=False, + static_data_name='number_of_builds', + static_data_template=number_of_builds_template) + + self.add_column(title='Last build outcome', + help_text='Indicates whether the last project build \ + completed successfully or failed', + hideable=True, + orderable=False, + static_data_name='last_build_outcome', + static_data_template=last_build_outcome_template) + + self.add_column(title='Recipe', + help_text='The last recipe which was built in this \ + project', + hideable=True, + orderable=False, + static_data_name='recipe_name', + static_data_template=recipe_template) + + self.add_column(title='Errors', + help_text='The number of errors encountered during \ + the last project build (if any)', + hideable=True, + orderable=False, + static_data_name='errors', + static_data_template=errors_template) + + self.add_column(title='Warnings', + help_text='The number of warnings encountered during \ + the last project build (if any)', + hideable=True, + hidden=True, + orderable=False, + static_data_name='warnings', + static_data_template=warnings_template) + + self.add_column(title='Image files', + help_text='The root file system types produced by \ + the last project build', + hideable=True, + hidden=True, + orderable=False, + static_data_name='image_files', + static_data_template=image_files_template) + +class BuildsTable(ToasterTable): + """Table of builds in Toaster""" + + def __init__(self, *args, **kwargs): + super(BuildsTable, self).__init__(*args, **kwargs) + self.default_orderby = '-completed_on' + self.static_context_extra['Build'] = Build + self.static_context_extra['Task'] = Task + + # attributes that are overridden in subclasses + + # title for the page + self.title = '' + + # 'project' or 'all'; determines how the mrb (most recent builds) + # section is displayed + self.mrb_type = '' + + def get_builds(self): + """ + overridden in ProjectBuildsTable to return builds for a + single project + """ + return Build.objects.all() + + def get_context_data(self, **kwargs): + context = super(BuildsTable, self).get_context_data(**kwargs) + + # should be set in subclasses + context['mru'] = [] + + context['mrb_type'] = self.mrb_type + + return context + + def setup_queryset(self, *args, **kwargs): + """ + The queryset is annotated so that it can be sorted by number of + errors and number of warnings; but note that the criteria for + finding the log messages to populate these fields should match those + used in the Build model (orm/models.py) to populate the errors and + warnings properties + """ + queryset = self.get_builds() + + # Don't include in progress builds pr cancelled builds + queryset = queryset.exclude(Q(outcome=Build.IN_PROGRESS) | + Q(outcome=Build.CANCELLED)) + + # sort + queryset = queryset.order_by(self.default_orderby) + + # annotate with number of ERROR, EXCEPTION and CRITICAL log messages + criteria = (Q(logmessage__level=LogMessage.ERROR) | + Q(logmessage__level=LogMessage.EXCEPTION) | + Q(logmessage__level=LogMessage.CRITICAL)) + + queryset = queryset.annotate( + errors_no=Count( + Case( + When(criteria, then=Value(1)), + output_field=IntegerField() + ) + ) + ) + + # annotate with number of WARNING log messages + queryset = queryset.annotate( + warnings_no=Count( + Case( + When(logmessage__level=LogMessage.WARNING, then=Value(1)), + output_field=IntegerField() + ) + ) + ) + + self.queryset = queryset + + def setup_columns(self, *args, **kwargs): + outcome_template = ''' + <a href="{% url "builddashboard" data.id %}"> + {% if data.outcome == data.SUCCEEDED %} + <i class="icon-ok-sign success"></i> + {% elif data.outcome == data.FAILED %} + <i class="icon-minus-sign error"></i> + {% endif %} + </a> + + {% if data.cooker_log_path %} + + <a href="{% url "build_artifact" data.id "cookerlog" data.id %}"> + <i class="icon-download-alt get-help" + data-original-title="Download build log"></i> + </a> + {% endif %} + ''' + + recipe_template = ''' + {% for target_label in data.target_labels %} + <a href="{% url "builddashboard" data.id %}"> + {{target_label}} + </a> + <br /> + {% endfor %} + ''' + + machine_template = ''' + <a href="{% url "builddashboard" data.id %}"> + {{data.machine}} + </a> + ''' + + started_on_template = ''' + <a href="{% url "builddashboard" data.id %}"> + {{data.started_on | date:"d/m/y H:i"}} + </a> + ''' + + completed_on_template = ''' + <a href="{% url "builddashboard" data.id %}"> + {{data.completed_on | date:"d/m/y H:i"}} + </a> + ''' + + failed_tasks_template = ''' + {% if data.failed_tasks.count == 1 %} + <a href="{% url "task" data.id data.failed_tasks.0.id %}"> + <span class="error"> + {{data.failed_tasks.0.recipe.name}}.{{data.failed_tasks.0.task_name}} + </span> + </a> + <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}"> + <i class="icon-download-alt" + data-original-title="Download task log file"> + </i> + </a> + {% elif data.failed_tasks.count > 1 %} + <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}"> + <span class="error">{{data.failed_tasks.count}} tasks</span> + </a> + {% endif %} + ''' + + errors_template = ''' + {% if data.errors_no %} + <a class="errors.count error" href="{% url "builddashboard" data.id %}#errors"> + {{data.errors_no}} error{{data.errors_no|pluralize}} + </a> + {% endif %} + ''' + + warnings_template = ''' + {% if data.warnings_no %} + <a class="warnings.count warning" href="{% url "builddashboard" data.id %}#warnings"> + {{data.warnings_no}} warning{{data.warnings_no|pluralize}} + </a> + {% endif %} + ''' + + time_template = ''' + {% load projecttags %} + <a href="{% url "buildtime" data.id %}"> + {{data.timespent_seconds | sectohms}} + </a> + ''' + + image_files_template = ''' + {% if data.outcome == extra.Build.SUCCEEDED %} + <a href="{% url "builddashboard" data.id %}#images"> + {{data.get_image_file_extensions}} + </a> + {% endif %} + ''' + + self.add_column(title='Outcome', + help_text='Final state of the build (successful \ + or failed)', + hideable=False, + orderable=True, + filter_name='outcome_filter', + static_data_name='outcome', + static_data_template=outcome_template) + + self.add_column(title='Recipe', + help_text='What was built (i.e. one or more recipes \ + or image recipes)', + hideable=False, + orderable=False, + static_data_name='target', + static_data_template=recipe_template) + + self.add_column(title='Machine', + help_text='Hardware for which you are building a \ + recipe or image recipe', + hideable=False, + orderable=True, + static_data_name='machine', + static_data_template=machine_template) + + self.add_column(title='Started on', + help_text='The date and time when the build started', + hideable=True, + hidden=True, + orderable=True, + filter_name='started_on_filter', + static_data_name='started_on', + static_data_template=started_on_template) + + self.add_column(title='Completed on', + help_text='The date and time when the build finished', + hideable=False, + orderable=True, + filter_name='completed_on_filter', + static_data_name='completed_on', + static_data_template=completed_on_template) + + self.add_column(title='Failed tasks', + help_text='The number of tasks which failed during \ + the build', + hideable=True, + orderable=False, + filter_name='failed_tasks_filter', + static_data_name='failed_tasks', + static_data_template=failed_tasks_template) + + self.add_column(title='Errors', + help_text='The number of errors encountered during \ + the build (if any)', + hideable=True, + orderable=True, + static_data_name='errors_no', + static_data_template=errors_template) + + self.add_column(title='Warnings', + help_text='The number of warnings encountered during \ + the build (if any)', + hideable=True, + orderable=True, + static_data_name='warnings_no', + static_data_template=warnings_template) + + self.add_column(title='Time', + help_text='How long the build took to finish', + hideable=True, + hidden=True, + orderable=False, + static_data_name='time', + static_data_template=time_template) + + self.add_column(title='Image files', + help_text='The root file system types produced by \ + the build', + hideable=True, + orderable=False, + static_data_name='image_files', + static_data_template=image_files_template) + + def setup_filters(self, *args, **kwargs): + # outcomes + outcome_filter = TableFilter( + 'outcome_filter', + 'Filter builds by outcome' + ) + + successful_builds_action = TableFilterActionToggle( + 'successful_builds', + 'Successful builds', + Q(outcome=Build.SUCCEEDED) + ) + + failed_builds_action = TableFilterActionToggle( + 'failed_builds', + 'Failed builds', + Q(outcome=Build.FAILED) + ) + + outcome_filter.add_action(successful_builds_action) + outcome_filter.add_action(failed_builds_action) + self.add_filter(outcome_filter) + + # started on + started_on_filter = TableFilter( + 'started_on_filter', + 'Filter by date when build was started' + ) + + started_today_action = TableFilterActionDay( + 'today', + 'Today\'s builds', + 'started_on', + 'today' + ) + + started_yesterday_action = TableFilterActionDay( + 'yesterday', + 'Yesterday\'s builds', + 'started_on', + 'yesterday' + ) + + by_started_date_range_action = TableFilterActionDateRange( + 'date_range', + 'Build date range', + 'started_on' + ) + + started_on_filter.add_action(started_today_action) + started_on_filter.add_action(started_yesterday_action) + started_on_filter.add_action(by_started_date_range_action) + self.add_filter(started_on_filter) + + # completed on + completed_on_filter = TableFilter( + 'completed_on_filter', + 'Filter by date when build was completed' + ) + + completed_today_action = TableFilterActionDay( + 'today', + 'Today\'s builds', + 'completed_on', + 'today' + ) + + completed_yesterday_action = TableFilterActionDay( + 'yesterday', + 'Yesterday\'s builds', + 'completed_on', + 'yesterday' + ) + + by_completed_date_range_action = TableFilterActionDateRange( + 'date_range', + 'Build date range', + 'completed_on' + ) + + completed_on_filter.add_action(completed_today_action) + completed_on_filter.add_action(completed_yesterday_action) + completed_on_filter.add_action(by_completed_date_range_action) + self.add_filter(completed_on_filter) + + # failed tasks + failed_tasks_filter = TableFilter( + 'failed_tasks_filter', + 'Filter builds by failed tasks' + ) + + criteria = Q(task_build__outcome=Task.OUTCOME_FAILED) + + with_failed_tasks_action = TableFilterActionToggle( + 'with_failed_tasks', + 'Builds with failed tasks', + criteria + ) + + without_failed_tasks_action = TableFilterActionToggle( + 'without_failed_tasks', + 'Builds without failed tasks', + ~criteria + ) + + failed_tasks_filter.add_action(with_failed_tasks_action) + failed_tasks_filter.add_action(without_failed_tasks_action) + self.add_filter(failed_tasks_filter) + + +class AllBuildsTable(BuildsTable): + """ Builds page for all builds """ + + def __init__(self, *args, **kwargs): + super(AllBuildsTable, self).__init__(*args, **kwargs) + self.title = 'All builds' + self.mrb_type = 'all' + + def setup_columns(self, *args, **kwargs): + """ + All builds page shows a column for the project + """ + + super(AllBuildsTable, self).setup_columns(*args, **kwargs) + + project_template = ''' + {% load project_url_tag %} + <a href="{% project_url data.project %}"> + {{data.project.name}} + </a> + {% if data.project.is_default %} + <i class="icon-question-sign get-help hover-help" title="" + data-original-title="This project shows information about + the builds you start from the command line while Toaster is + running" style="visibility: hidden;"></i> + {% endif %} + ''' + + self.add_column(title='Project', + hideable=True, + orderable=True, + static_data_name='project', + static_data_template=project_template) + + def get_context_data(self, **kwargs): + """ Get all builds for the recent builds area """ + context = super(AllBuildsTable, self).get_context_data(**kwargs) + context['mru'] = Build.get_recent() + return context + +class ProjectBuildsTable(BuildsTable): + """ + Builds page for a single project; a BuildsTable, with the queryset + filtered by project + """ + + def __init__(self, *args, **kwargs): + super(ProjectBuildsTable, self).__init__(*args, **kwargs) + self.title = 'All project builds' + self.mrb_type = 'project' + + # set from the querystring + self.project_id = None + + def setup_columns(self, *args, **kwargs): + """ + Project builds table doesn't show the machines column by default + """ + + super(ProjectBuildsTable, self).setup_columns(*args, **kwargs) + + # hide the machine column + self.set_column_hidden('Machine', True) + + # allow the machine column to be hidden by the user + self.set_column_hideable('Machine', True) + + def setup_queryset(self, *args, **kwargs): + """ + NOTE: self.project_id must be set before calling super(), + as it's used in setup_queryset() + """ + self.project_id = kwargs['pid'] + super(ProjectBuildsTable, self).setup_queryset(*args, **kwargs) + + project = Project.objects.get(pk=self.project_id) + self.queryset = self.queryset.filter(project=project) + + def get_context_data(self, **kwargs): + """ + Get recent builds for this project, and the project itself + + NOTE: self.project_id must be set before calling super(), + as it's used in get_context_data() + """ + self.project_id = kwargs['pid'] + context = super(ProjectBuildsTable, self).get_context_data(**kwargs) + + project = Project.objects.get(pk=self.project_id) + context['mru'] = Build.get_recent(project) + context['project'] = project + + return context |