From eaae82a19acaf786b866043ea80107f28e8206b2 Mon Sep 17 00:00:00 2001 From: Elliot Smith Date: Fri, 15 Jan 2016 13:00:55 +0200 Subject: bitbake: toastergui: convert project builds page to ToasterTable Use the all builds ToasterTable as the basis for the project builds ToasterTable. [YOCTO #8738] (Bitbake rev: 87bcfb740dd2d9944e35a2a1f71cbf8ff3b266e9) Signed-off-by: Elliot Smith Signed-off-by: Ed Bartosh Signed-off-by: Richard Purdie --- .../toaster/toastergui/static/js/projecttopbar.js | 9 + bitbake/lib/toaster/toastergui/static/js/table.js | 13 +- bitbake/lib/toaster/toastergui/tables.py | 184 +++++++++++++++++---- .../toastergui/templates/baseprojectpage.html | 1 + .../toaster/toastergui/templates/mrb_section.html | 2 +- .../templates/projectbuilds-toastertable.html | 56 +++++++ bitbake/lib/toaster/toastergui/urls.py | 6 +- bitbake/lib/toaster/toastergui/views.py | 16 +- bitbake/lib/toaster/toastergui/widgets.py | 1 - 9 files changed, 239 insertions(+), 49 deletions(-) create mode 100644 bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html (limited to 'bitbake') diff --git a/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js b/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js index b6ad380c19..58a32a056e 100644 --- a/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js +++ b/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js @@ -7,7 +7,10 @@ function projectTopBarInit(ctx) { var projectName = $("#project-name"); var projectNameFormToggle = $("#project-change-form-toggle"); var projectNameChangeCancel = $("#project-name-change-cancel"); + + // this doesn't exist for command-line builds var newBuildTargetInput = $("#build-input"); + var newBuildTargetBuildBtn = $("#build-button"); var selectedTarget; @@ -42,6 +45,12 @@ function projectTopBarInit(ctx) { $(this).parent().removeClass('active'); }); + if (!newBuildTargetInput.length) { + return; + } + + /* the following only applies for non-command-line projects */ + /* Recipe build input functionality */ if (ctx.numProjectLayers > 0 && ctx.machine){ newBuildTargetInput.removeAttr("disabled"); diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js index afe16b5e1b..7ac4ed5859 100644 --- a/bitbake/lib/toaster/toastergui/static/js/table.js +++ b/bitbake/lib/toaster/toastergui/static/js/table.js @@ -33,6 +33,10 @@ function tableInit(ctx){ loadData(tableParams); + // clicking on this set of elements removes the search + var clearSearchElements = $('.remove-search-btn-'+ctx.tableName + + ', .show-all-'+ctx.tableName); + function loadData(tableParams){ $.ajax({ type: "GET", @@ -62,9 +66,9 @@ function tableInit(ctx){ paginationBtns.html(""); if (tableParams.search) - $('.remove-search-btn-'+ctx.tableName).show(); + clearSearchElements.show(); else - $('.remove-search-btn-'+ctx.tableName).hide(); + clearSearchElements.hide(); $('.table-count-' + ctx.tableName).text(tableData.total); tableTotal = tableData.total; @@ -230,9 +234,8 @@ function tableInit(ctx){ } else { /* Not orderable */ - header.addClass("muted"); header.css("font-weight", "normal"); - header.append(col.title+' '); + header.append('' + col.title + ' '); } /* Setup the filter button */ @@ -665,7 +668,7 @@ function tableInit(ctx){ loadData(tableParams); }); - $('.remove-search-btn-'+ctx.tableName).click(function(e){ + clearSearchElements.click(function(e){ e.preventDefault(); tableParams.page = 1; diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py index 58abe36b05..d0ed49625d 100644 --- a/bitbake/lib/toaster/toastergui/tables.py +++ b/bitbake/lib/toaster/toastergui/tables.py @@ -23,9 +23,11 @@ from toastergui.widgets import ToasterTable from toastergui.querysetfilter import QuerysetFilter from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project from orm.models import CustomImageRecipe, Package, Build, LogMessage, Task +from orm.models import ProjectTarget from django.db.models import Q, Max, Count 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 import itertools @@ -775,7 +777,7 @@ class ProjectsTable(ToasterTable): ''' errors_template = ''' - {% if data.get_number_of_builds > 0 %} + {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %} {{data.get_last_errors}} error{{data.get_last_errors | pluralize}} @@ -784,7 +786,7 @@ class ProjectsTable(ToasterTable): ''' warnings_template = ''' - {% if data.get_number_of_builds > 0 %} + {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %} {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}} @@ -886,30 +888,45 @@ class BuildsTable(ToasterTable): def __init__(self, *args, **kwargs): super(BuildsTable, self).__init__(*args, **kwargs) self.default_orderby = '-completed_on' - self.title = 'All builds' 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) # for the latest builds section - queryset = Build.objects.all() + builds = self.get_builds() finished_criteria = Q(outcome=Build.SUCCEEDED) | Q(outcome=Build.FAILED) latest_builds = itertools.chain( - queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"), - queryset.filter(finished_criteria).order_by("-completed_on")[:3] + builds.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"), + builds.filter(finished_criteria).order_by("-completed_on")[:3] ) context['mru'] = list(latest_builds) - context['mrb_type'] = 'all' + context['mrb_type'] = self.mrb_type return context def setup_queryset(self, *args, **kwargs): - queryset = Build.objects.all() + queryset = self.get_builds() # don't include in progress builds queryset = queryset.exclude(outcome=Build.IN_PROGRESS) @@ -949,7 +966,8 @@ class BuildsTable(ToasterTable): {% if data.cooker_log_path %}   - + {% endif %} ''' @@ -1031,19 +1049,6 @@ class BuildsTable(ToasterTable): {% endif %} ''' - project_template = ''' - {% load project_url_tag %} - - {{data.project.name}} - - {% if data.project.is_default %} - - {% endif %} - ''' - self.add_column(title='Outcome', help_text='Final state of the build (successful \ or failed)', @@ -1098,16 +1103,16 @@ class BuildsTable(ToasterTable): help_text='The number of errors encountered during \ the build (if any)', hideable=True, - orderable=False, - static_data_name='errors', + 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=False, - static_data_name='warnings', + orderable=True, + static_data_name='warnings_no', static_data_template=warnings_template) self.add_column(title='Time', @@ -1125,12 +1130,6 @@ class BuildsTable(ToasterTable): static_data_name='image_files', static_data_template=image_files_template) - self.add_column(title='Project', - hideable=True, - orderable=False, - static_data_name='project-name', - static_data_template=project_template) - def setup_filters(self, *args, **kwargs): # outcomes outcome_filter = TableFilter( @@ -1239,3 +1238,122 @@ class BuildsTable(ToasterTable): failed_tasks_filter.add_action(with_failed_tasks_action) failed_tasks_filter.add_action(without_failed_tasks_action) self.add_filter(failed_tasks_filter) + + def post(self, request, *args, **kwargs): + """ Process HTTP POSTs which make build requests """ + + project = Project.objects.get(pk=kwargs['pid']) + + if 'buildCancel' in request.POST: + for i in request.POST['buildCancel'].strip().split(" "): + try: + br = BuildRequest.objects.select_for_update().get(project = project, pk = i, state__lte = BuildRequest.REQ_QUEUED) + br.state = BuildRequest.REQ_DELETED + br.save() + except BuildRequest.DoesNotExist: + pass + + if 'buildDelete' in request.POST: + for i in request.POST['buildDelete'].strip().split(" "): + try: + BuildRequest.objects.select_for_update().get(project = project, pk = i, state__lte = BuildRequest.REQ_DELETED).delete() + except BuildRequest.DoesNotExist: + pass + + if 'targets' in request.POST: + ProjectTarget.objects.filter(project = project).delete() + s = str(request.POST['targets']) + for t in s.translate(None, ";%|\"").split(" "): + if ":" in t: + target, task = t.split(":") + else: + target = t + task = "" + ProjectTarget.objects.create(project = project, + target = target, + task = task) + project.schedule_build() + + # redirect back to builds page so any new builds in progress etc. + # are visible + response = HttpResponse() + response.status_code = 302 + response['Location'] = request.build_absolute_uri() + return response + +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 %} + + {{data.project.name}} + + {% if data.project.is_default %} + + {% endif %} + ''' + + self.add_column(title='Project', + hideable=True, + orderable=True, + static_data_name='project', + static_data_template=project_template) + +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_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): + """ + 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) + context['project'] = Project.objects.get(pk=self.project_id) + + return context + + def get_builds(self): + """ override: only return builds for the relevant project """ + + project = Project.objects.get(pk=self.project_id) + return Build.objects.filter(project=project) diff --git a/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html b/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html index 1f45be462d..b143b78833 100644 --- a/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html +++ b/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html @@ -1,4 +1,5 @@ {% extends "base.html" %} + {% load projecttags %} {% load humanize %} diff --git a/bitbake/lib/toaster/toastergui/templates/mrb_section.html b/bitbake/lib/toaster/toastergui/templates/mrb_section.html index 52b3f1a7d3..2f4820c3e7 100644 --- a/bitbake/lib/toaster/toastergui/templates/mrb_section.html +++ b/bitbake/lib/toaster/toastergui/templates/mrb_section.html @@ -6,7 +6,7 @@ {%if mru and mru.count > 0%} {%if mrb_type == 'project' %} -

+