Use the all builds ToasterTable as the basis for the project builds ToasterTable.
[YOCTO #8738] Signed-off-by: Elliot Smith <[email protected]> --- .../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 diff --git a/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js b/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js index b6ad380..58a32a0 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 afe16b5..7ac4ed5 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('<span class="muted">' + col.title + '</span> '); } /* 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 ca43408..dd896fe 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 @@ -777,7 +779,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 %} <a class="errors.count error" href="{% url "builddashboard" data.get_last_build_id %}#errors"> {{data.get_last_errors}} error{{data.get_last_errors | pluralize}} @@ -786,7 +788,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 %} <a class="warnings.count warning" href="{% url "builddashboard" data.get_last_build_id %}#warnings"> {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}} @@ -888,30 +890,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) @@ -951,7 +968,8 @@ class BuildsTable(ToasterTable): {% if data.cooker_log_path %} <a href="{% url "build_artifact" data.id "cookerlog" data.id %}"> - <i class="icon-download-alt" title="Download build log"></i> + <i class="icon-download-alt get-help" + data-original-title="Download build log"></i> </a> {% endif %} ''' @@ -1033,19 +1051,6 @@ class BuildsTable(ToasterTable): {% endif %} ''' - 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='Outcome', help_text='Final state of the build (successful \ or failed)', @@ -1100,16 +1105,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', @@ -1127,12 +1132,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( @@ -1241,3 +1240,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 %} + <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) + +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 1f45be4..b143b78 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 52b3f1a..2f4820c 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' %} - <h2> + <h2 class="page-header"> Latest project builds {% if project.is_default %} diff --git a/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html b/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html new file mode 100644 index 0000000..6d7e10b --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html @@ -0,0 +1,56 @@ +{% extends 'base.html' %} + +{% load static %} + +{% block extraheadcontent %} + <link rel="stylesheet" href="{% static 'css/jquery-ui.min.css' %}" type='text/css'> + <link rel="stylesheet" href="{% static 'css/jquery-ui.structure.min.css' %}" type='text/css'> + <link rel="stylesheet" href="{% static 'css/jquery-ui.theme.min.css' %}" type='text/css'> + <script src="{% static 'js/jquery-ui.min.js' %}"> + </script> +{% endblock %} + +{% block title %} {{title}} - {{project.name}} - Toaster {% endblock %} + +{% block pagecontent %} + + {% include "projecttopbar.html" %} + + <div class="row-fluid"> + {% with mru=mru mrb_type=mrb_type %} + {% include 'mrb_section.html' %} + {% endwith %} + + <h2 class="page-header top-air" data-role="page-title"></h2> + + {% url 'projectbuilds' project.id as xhr_table_url %} + {% include 'toastertable.html' %} + </div> + + <script> + $(document).ready(function () { + // title + var tableElt = $("#{{table_name}}"); + var titleElt = $("[data-role='page-title']"); + + tableElt.on("table-done", function (e, total, tableParams) { + var title = "All project builds"; + + if (tableParams.search || tableParams.filter) { + if (total === 0) { + title = "No project builds found"; + } + else if (total > 0) { + title = total + " project build" + (total > 1 ? 's' : '') + " found"; + } + } + + titleElt.text(title); + }); + + // highlight builds tab + $("#topbar-builds-tab").addClass("active") + }); + </script> + +{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py index 707b7d5..c8c1c6a 100644 --- a/bitbake/lib/toaster/toastergui/urls.py +++ b/bitbake/lib/toaster/toastergui/urls.py @@ -28,7 +28,7 @@ urlpatterns = patterns('toastergui.views', url(r'^landing/$', 'landing', name='landing'), url(r'^builds/$', - tables.BuildsTable.as_view(template_name="builds-toastertable.html"), + tables.AllBuildsTable.as_view(template_name="builds-toastertable.html"), name='all-builds'), # build info navigation @@ -83,7 +83,9 @@ urlpatterns = patterns('toastergui.views', url(r'^project/(?P<pid>\d+)/$', 'project', name='project'), url(r'^project/(?P<pid>\d+)/configuration$', 'projectconf', name='projectconf'), - url(r'^project/(?P<pid>\d+)/builds/$', 'projectbuilds', name='projectbuilds'), + url(r'^project/(?P<pid>\d+)/builds/$', + tables.ProjectBuildsTable.as_view(template_name="projectbuilds-toastertable.html"), + name='projectbuilds'), # the import layer is a project-specific functionality; url(r'^project/(?P<pid>\d+)/importlayer$', 'importlayer', name='importlayer'), diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py index 295773f..fbae36c 100755 --- a/bitbake/lib/toaster/toastergui/views.py +++ b/bitbake/lib/toaster/toastergui/views.py @@ -91,6 +91,7 @@ def landing(request): return render(request, 'landing.html', context) +""" # returns a list for most recent builds; def _get_latest_builds(prj=None): queryset = Build.objects.all() @@ -101,8 +102,9 @@ def _get_latest_builds(prj=None): return list(itertools.chain( queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"), queryset.filter(outcome__lt=Build.IN_PROGRESS).order_by("-started_on")[:3] )) +""" - +""" # a JSON-able dict of recent builds; for use in the Project page, xhr_ updates, and other places, as needed def _project_recent_build_list(prj): data = [] @@ -131,8 +133,7 @@ def _project_recent_build_list(prj): data.append(d) return data - - +""" def objtojson(obj): from django.db.models.query import QuerySet @@ -1915,6 +1916,7 @@ if True: ''' The exception raised on invalid POST requests ''' pass + """ # helper function, to be used on "all builds" and "project builds" pages def _build_list_helper(request, queryset_all, redirect_page, pid=None): default_orderby = 'completed_on:-' @@ -2119,6 +2121,7 @@ if True: # merge daterange values context.update(context_date) return context, pagesize, orderby + """ @@ -2256,7 +2259,7 @@ if True: "completedbuilds": Build.objects.exclude(outcome = Build.IN_PROGRESS).filter(project_id = pid), "prj" : {"name": prj.name, }, "buildrequests" : prj.build_set.filter(outcome=Build.IN_PROGRESS), - "builds" : _project_recent_build_list(prj), + #"builds" : _project_recent_build_list(prj), "layers" : map(lambda x: { "id": x.layercommit.pk, "orderid": x.pk, @@ -2827,10 +2830,8 @@ if True: # will set the GET parameters and redirect back to the # all-builds or projectbuilds page as appropriate; # TODO don't use exceptions to control program flow - @_template_renderer('projectbuilds.html') + """ def projectbuilds(request, pid): - prj = Project.objects.get(id = pid) - if request.method == "POST": # process any build request @@ -2880,6 +2881,7 @@ if True: context['mru'] = _get_latest_builds(prj) return context + """ def _file_name_for_artifact(b, artifact_type, artifact_id): diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py index 47de30d..bc081b8 100644 --- a/bitbake/lib/toaster/toastergui/widgets.py +++ b/bitbake/lib/toaster/toastergui/widgets.py @@ -61,7 +61,6 @@ class ToasterTable(TemplateView): self.total_count = 0 self.static_context_extra = {} - self.filter_actions = {} self.empty_state = "Sorry - no data found" self.default_orderby = "" -- Elliot Smith Software Engineer Intel OTC --------------------------------------------------------------------- Intel Corporation (UK) Limited Registered No. 1134945 (England) Registered Office: Pipers Way, Swindon SN3 1RJ VAT No: 860 2173 47 This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). Any review or distribution by others is strictly prohibited. If you are not the intended recipient, please contact the sender and delete all copies. -- _______________________________________________ toaster mailing list [email protected] https://lists.yoctoproject.org/listinfo/toaster
