mirror of
https://git.yoctoproject.org/poky
synced 2026-03-01 21:09:39 +01:00
bitbake: toastergui: refactor ToasterTable filtering
The filter code for ToasterTable was difficult to follow and inflexible (not allowing different types of filter, for example). Refactor to a set of filter classes to make the structure cleaner and provide the flexibility needed for other filter types (e.g. date range filter). [YOCTO #8738] (Bitbake rev: 94031bb30bdaf665d0c8c68b591fcb7a17b6674d) Signed-off-by: Elliot Smith <elliot.smith@intel.com> Signed-off-by: Ed Bartosh <ed.bartosh@linux.intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
committed by
Richard Purdie
parent
294579b531
commit
809046c6fb
@@ -5,7 +5,7 @@ class QuerysetFilter(object):
|
||||
if criteria:
|
||||
self.set_criteria(criteria)
|
||||
|
||||
def set_criteria(self, criteria):
|
||||
def set_criteria(self, criteria = None):
|
||||
"""
|
||||
criteria is an instance of django.db.models.Q;
|
||||
see https://docs.djangoproject.com/en/1.9/ref/models/querysets/#q-objects
|
||||
@@ -17,7 +17,10 @@ class QuerysetFilter(object):
|
||||
Filter queryset according to the criteria for this filter,
|
||||
returning the filtered queryset
|
||||
"""
|
||||
return queryset.filter(self.criteria)
|
||||
if self.criteria:
|
||||
return queryset.filter(self.criteria)
|
||||
else:
|
||||
return queryset
|
||||
|
||||
def count(self, queryset):
|
||||
""" Returns a count of the elements in the filtered queryset """
|
||||
|
||||
@@ -415,38 +415,76 @@ function tableInit(ctx){
|
||||
data: params,
|
||||
headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
|
||||
success: function (filterData) {
|
||||
/*
|
||||
filterData structure:
|
||||
|
||||
{
|
||||
title: '<title for the filter popup>',
|
||||
filter_actions: [
|
||||
{
|
||||
title: '<label for radio button inside the popup>',
|
||||
name: '<name of the filter action>',
|
||||
count: <number of items this filter will show>
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
each filter_action gets a radio button; the value of this is
|
||||
set to filterName + ':' + filter_action.name; e.g.
|
||||
|
||||
in_current_project:in_project
|
||||
|
||||
specifies the "in_project" action of the "in_current_project"
|
||||
filter
|
||||
|
||||
the filterName is set on the column filter icon, and corresponds
|
||||
to a value in the table's filters property
|
||||
|
||||
when the filter popup's "Apply" button is clicked, the
|
||||
value for the radio button which is checked is passed in the
|
||||
querystring and applied to the queryset on the table
|
||||
*/
|
||||
|
||||
var filterActionRadios = $('#filter-actions-'+ctx.tableName);
|
||||
|
||||
$('#filter-modal-title-'+ctx.tableName).text(filterData.title);
|
||||
|
||||
filterActionRadios.text("");
|
||||
|
||||
for (var i in filterData.filter_actions){
|
||||
for (var i in filterData.filter_actions) {
|
||||
var filterAction = filterData.filter_actions[i];
|
||||
var action = null;
|
||||
|
||||
var action = $('<label class="radio"><input type="radio" name="filter" value=""><span class="filter-title"></span></label>');
|
||||
var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
|
||||
if (filterAction.type === 'toggle') {
|
||||
var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
|
||||
|
||||
var radioInput = action.children("input");
|
||||
action = $('<label class="radio">' +
|
||||
'<input type="radio" name="filter" value="">' +
|
||||
'<span class="filter-title">' +
|
||||
actionTitle +
|
||||
'</span>' +
|
||||
'</label>');
|
||||
|
||||
if (Number(filterAction.count) == 0){
|
||||
radioInput.attr("disabled", "disabled");
|
||||
var radioInput = action.children("input");
|
||||
if (Number(filterAction.count) == 0) {
|
||||
radioInput.attr("disabled", "disabled");
|
||||
}
|
||||
|
||||
radioInput.val(filterData.name + ':' + filterAction.action_name);
|
||||
|
||||
/* Setup the current selected filter, default to 'all' if
|
||||
* no current filter selected.
|
||||
*/
|
||||
if ((tableParams.filter &&
|
||||
tableParams.filter === radioInput.val()) ||
|
||||
filterAction.action_name == 'all') {
|
||||
radioInput.attr("checked", "checked");
|
||||
}
|
||||
}
|
||||
|
||||
action.children(".filter-title").text(actionTitle);
|
||||
|
||||
radioInput.val(filterName + ':' + filterAction.name);
|
||||
|
||||
/* Setup the current selected filter, default to 'all' if
|
||||
* no current filter selected.
|
||||
*/
|
||||
if ((tableParams.filter &&
|
||||
tableParams.filter === radioInput.val()) ||
|
||||
filterAction.name == 'all') {
|
||||
radioInput.attr("checked", "checked");
|
||||
if (action) {
|
||||
filterActionRadios.append(action);
|
||||
}
|
||||
|
||||
filterActionRadios.append(action);
|
||||
}
|
||||
|
||||
$('#filter-modal-'+ctx.tableName).modal('show');
|
||||
|
||||
119
bitbake/lib/toaster/toastergui/tablefilter.py
Normal file
119
bitbake/lib/toaster/toastergui/tablefilter.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#
|
||||
# ex:ts=4:sw=4:sts=4:et
|
||||
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2015 Intel Corporation
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License version 2 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
class TableFilter(object):
|
||||
"""
|
||||
Stores a filter for a named field, and can retrieve the action
|
||||
requested for that filter
|
||||
"""
|
||||
def __init__(self, name, title):
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.__filter_action_map = {}
|
||||
|
||||
def add_action(self, action):
|
||||
self.__filter_action_map[action.name] = action
|
||||
|
||||
def get_action(self, action_name):
|
||||
return self.__filter_action_map[action_name]
|
||||
|
||||
def to_json(self, queryset):
|
||||
"""
|
||||
Dump all filter actions as an object which can be JSON serialised;
|
||||
this is used to generate the JSON for processing in
|
||||
table.js / filterOpenClicked()
|
||||
"""
|
||||
filter_actions = []
|
||||
|
||||
# add the "all" pseudo-filter action, which just selects the whole
|
||||
# queryset
|
||||
filter_actions.append({
|
||||
'action_name' : 'all',
|
||||
'title' : 'All',
|
||||
'type': 'toggle',
|
||||
'count' : queryset.count()
|
||||
})
|
||||
|
||||
# add other filter actions
|
||||
for action_name, filter_action in self.__filter_action_map.iteritems():
|
||||
obj = filter_action.to_json(queryset)
|
||||
obj['action_name'] = action_name
|
||||
filter_actions.append(obj)
|
||||
|
||||
return {
|
||||
'name': self.name,
|
||||
'title': self.title,
|
||||
'filter_actions': filter_actions
|
||||
}
|
||||
|
||||
class TableFilterActionToggle(object):
|
||||
"""
|
||||
Stores a single filter action which will populate one radio button of
|
||||
a ToasterTable filter popup; this filter can either be on or off and
|
||||
has no other parameters
|
||||
"""
|
||||
|
||||
def __init__(self, name, title, queryset_filter):
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.__queryset_filter = queryset_filter
|
||||
self.type = 'toggle'
|
||||
|
||||
def set_params(self, params):
|
||||
"""
|
||||
params: (str) a string of extra parameters for the action;
|
||||
the structure of this string depends on the type of action;
|
||||
it's ignored for a toggle filter action, which is just on or off
|
||||
"""
|
||||
pass
|
||||
|
||||
def filter(self, queryset):
|
||||
return self.__queryset_filter.filter(queryset)
|
||||
|
||||
def to_json(self, queryset):
|
||||
""" Dump as a JSON object """
|
||||
return {
|
||||
'title': self.title,
|
||||
'type': self.type,
|
||||
'count': self.__queryset_filter.count(queryset)
|
||||
}
|
||||
|
||||
class TableFilterMap(object):
|
||||
"""
|
||||
Map from field names to Filter objects for those fields
|
||||
"""
|
||||
def __init__(self):
|
||||
self.__filters = {}
|
||||
|
||||
def add_filter(self, filter_name, table_filter):
|
||||
""" table_filter is an instance of Filter """
|
||||
self.__filters[filter_name] = table_filter
|
||||
|
||||
def get_filter(self, filter_name):
|
||||
return self.__filters[filter_name]
|
||||
|
||||
def to_json(self, queryset):
|
||||
data = {}
|
||||
|
||||
for filter_name, table_filter in self.__filters.iteritems():
|
||||
data[filter_name] = table_filter.to_json()
|
||||
|
||||
return data
|
||||
@@ -28,6 +28,8 @@ from django.conf.urls import url
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from toastergui.tablefilter import TableFilter, TableFilterActionToggle
|
||||
|
||||
class ProjectFilters(object):
|
||||
def __init__(self, project_layers):
|
||||
self.in_project = QuerysetFilter(Q(layer_version__in=project_layers))
|
||||
@@ -53,16 +55,28 @@ class LayersTable(ToasterTable):
|
||||
project = Project.objects.get(pk=kwargs['pid'])
|
||||
self.project_layers = ProjectLayer.objects.filter(project=project)
|
||||
|
||||
criteria = Q(projectlayer__in=self.project_layers)
|
||||
in_project_filter = QuerysetFilter(criteria)
|
||||
not_in_project_filter = QuerysetFilter(~criteria)
|
||||
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", in_project_filter),
|
||||
self.make_filter_action("not_in_project", "Layers not added to this project", not_in_project_filter)
|
||||
])
|
||||
criteria = Q(projectlayer__in=self.project_layers)
|
||||
|
||||
in_project_filter_action = TableFilterActionToggle(
|
||||
"in_project",
|
||||
"Layers added to this project",
|
||||
QuerysetFilter(criteria)
|
||||
)
|
||||
|
||||
not_in_project_filter_action = TableFilterActionToggle(
|
||||
"not_in_project",
|
||||
"Layers not added to this project",
|
||||
QuerysetFilter(~criteria)
|
||||
)
|
||||
|
||||
in_current_project_filter.add_action(in_project_filter_action)
|
||||
in_current_project_filter.add_action(not_in_project_filter_action)
|
||||
self.add_filter(in_current_project_filter)
|
||||
|
||||
def setup_queryset(self, *args, **kwargs):
|
||||
prj = Project.objects.get(pk = kwargs['pid'])
|
||||
@@ -199,12 +213,26 @@ class MachinesTable(ToasterTable):
|
||||
|
||||
project_filters = ProjectFilters(self.project_layers)
|
||||
|
||||
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", project_filters.in_project),
|
||||
self.make_filter_action("not_in_project", "Machines provided by layers not added to this project", project_filters.not_in_project)
|
||||
])
|
||||
in_current_project_filter = TableFilter(
|
||||
"in_current_project",
|
||||
"Filter by project machines"
|
||||
)
|
||||
|
||||
in_project_filter_action = TableFilterActionToggle(
|
||||
"in_project",
|
||||
"Machines provided by layers added to this project",
|
||||
project_filters.in_project
|
||||
)
|
||||
|
||||
not_in_project_filter_action = TableFilterActionToggle(
|
||||
"not_in_project",
|
||||
"Machines provided by layers not added to this project",
|
||||
project_filters.not_in_project
|
||||
)
|
||||
|
||||
in_current_project_filter.add_action(in_project_filter_action)
|
||||
in_current_project_filter.add_action(not_in_project_filter_action)
|
||||
self.add_filter(in_current_project_filter)
|
||||
|
||||
def setup_queryset(self, *args, **kwargs):
|
||||
prj = Project.objects.get(pk = kwargs['pid'])
|
||||
@@ -318,12 +346,26 @@ class RecipesTable(ToasterTable):
|
||||
def setup_filters(self, *args, **kwargs):
|
||||
project_filters = ProjectFilters(self.project_layers)
|
||||
|
||||
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", project_filters.in_project),
|
||||
self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", project_filters.not_in_project)
|
||||
])
|
||||
table_filter = TableFilter(
|
||||
'in_current_project',
|
||||
'Filter by project recipes'
|
||||
)
|
||||
|
||||
in_project_filter_action = TableFilterActionToggle(
|
||||
'in_project',
|
||||
'Recipes provided by layers added to this project',
|
||||
project_filters.in_project
|
||||
)
|
||||
|
||||
not_in_project_filter_action = TableFilterActionToggle(
|
||||
'not_in_project',
|
||||
'Recipes provided by layers not added to this project',
|
||||
project_filters.not_in_project
|
||||
)
|
||||
|
||||
table_filter.add_action(in_project_filter_action)
|
||||
table_filter.add_action(not_in_project_filter_action)
|
||||
self.add_filter(table_filter)
|
||||
|
||||
def setup_queryset(self, *args, **kwargs):
|
||||
prj = Project.objects.get(pk = kwargs['pid'])
|
||||
@@ -1070,47 +1112,47 @@ class BuildsTable(ToasterTable):
|
||||
|
||||
def setup_filters(self, *args, **kwargs):
|
||||
# outcomes
|
||||
filter_only_successful_builds = QuerysetFilter(Q(outcome=Build.SUCCEEDED))
|
||||
successful_builds_filter = self.make_filter_action(
|
||||
outcome_filter = TableFilter(
|
||||
'outcome_filter',
|
||||
'Filter builds by outcome'
|
||||
)
|
||||
|
||||
successful_builds_filter_action = TableFilterActionToggle(
|
||||
'successful_builds',
|
||||
'Successful builds',
|
||||
filter_only_successful_builds
|
||||
QuerysetFilter(Q(outcome=Build.SUCCEEDED))
|
||||
)
|
||||
|
||||
filter_only_failed_builds = QuerysetFilter(Q(outcome=Build.FAILED))
|
||||
failed_builds_filter = self.make_filter_action(
|
||||
failed_builds_filter_action = TableFilterActionToggle(
|
||||
'failed_builds',
|
||||
'Failed builds',
|
||||
filter_only_failed_builds
|
||||
QuerysetFilter(Q(outcome=Build.FAILED))
|
||||
)
|
||||
|
||||
self.add_filter(title='Filter builds by outcome',
|
||||
name='outcome_filter',
|
||||
filter_actions = [
|
||||
successful_builds_filter,
|
||||
failed_builds_filter
|
||||
])
|
||||
outcome_filter.add_action(successful_builds_filter_action)
|
||||
outcome_filter.add_action(failed_builds_filter_action)
|
||||
self.add_filter(outcome_filter)
|
||||
|
||||
# failed tasks
|
||||
failed_tasks_filter = TableFilter(
|
||||
'failed_tasks_filter',
|
||||
'Filter builds by failed tasks'
|
||||
)
|
||||
|
||||
criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
|
||||
filter_only_builds_with_failed_tasks = QuerysetFilter(criteria)
|
||||
with_failed_tasks_filter = self.make_filter_action(
|
||||
|
||||
with_failed_tasks_filter_action = TableFilterActionToggle(
|
||||
'with_failed_tasks',
|
||||
'Builds with failed tasks',
|
||||
filter_only_builds_with_failed_tasks
|
||||
QuerysetFilter(criteria)
|
||||
)
|
||||
|
||||
criteria = ~Q(task_build__outcome=Task.OUTCOME_FAILED)
|
||||
filter_only_builds_without_failed_tasks = QuerysetFilter(criteria)
|
||||
without_failed_tasks_filter = self.make_filter_action(
|
||||
without_failed_tasks_filter_action = TableFilterActionToggle(
|
||||
'without_failed_tasks',
|
||||
'Builds without failed tasks',
|
||||
filter_only_builds_without_failed_tasks
|
||||
QuerysetFilter(~criteria)
|
||||
)
|
||||
|
||||
self.add_filter(title='Filter builds by failed tasks',
|
||||
name='failed_tasks_filter',
|
||||
filter_actions = [
|
||||
with_failed_tasks_filter,
|
||||
without_failed_tasks_filter
|
||||
])
|
||||
failed_tasks_filter.add_action(with_failed_tasks_filter_action)
|
||||
failed_tasks_filter.add_action(without_failed_tasks_filter_action)
|
||||
self.add_filter(failed_tasks_filter)
|
||||
|
||||
@@ -39,11 +39,13 @@ import json
|
||||
import collections
|
||||
import operator
|
||||
import re
|
||||
import urllib
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("toaster")
|
||||
|
||||
from toastergui.views import objtojson
|
||||
from toastergui.tablefilter import TableFilterMap
|
||||
|
||||
class ToasterTable(TemplateView):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -53,7 +55,10 @@ class ToasterTable(TemplateView):
|
||||
self.title = "Table"
|
||||
self.queryset = None
|
||||
self.columns = []
|
||||
self.filters = {}
|
||||
|
||||
# map from field names to Filter instances
|
||||
self.filter_map = TableFilterMap()
|
||||
|
||||
self.total_count = 0
|
||||
self.static_context_extra = {}
|
||||
self.filter_actions = {}
|
||||
@@ -66,7 +71,7 @@ class ToasterTable(TemplateView):
|
||||
orderable=True,
|
||||
field_name="id")
|
||||
|
||||
# prevent HTTP caching of table data
|
||||
# prevent HTTP caching of table data
|
||||
@cache_control(must_revalidate=True, max_age=0, no_store=True, no_cache=True)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(ToasterTable, self).dispatch(*args, **kwargs)
|
||||
@@ -108,27 +113,10 @@ class ToasterTable(TemplateView):
|
||||
self.apply_search(search)
|
||||
|
||||
name = request.GET.get("name", None)
|
||||
if name is None:
|
||||
data = json.dumps(self.filters,
|
||||
indent=2,
|
||||
cls=DjangoJSONEncoder)
|
||||
else:
|
||||
for actions in self.filters[name]['filter_actions']:
|
||||
queryset_filter = self.filter_actions[actions['name']]
|
||||
actions['count'] = queryset_filter.count(self.queryset)
|
||||
|
||||
# Add the "All" items filter action
|
||||
self.filters[name]['filter_actions'].insert(0, {
|
||||
'name' : 'all',
|
||||
'title' : 'All',
|
||||
'count' : self.queryset.count(),
|
||||
})
|
||||
|
||||
data = json.dumps(self.filters[name],
|
||||
indent=2,
|
||||
cls=DjangoJSONEncoder)
|
||||
|
||||
return data
|
||||
table_filter = self.filter_map.get_filter(name)
|
||||
return json.dumps(table_filter.to_json(self.queryset),
|
||||
indent=2,
|
||||
cls=DjangoJSONEncoder)
|
||||
|
||||
def setup_columns(self, *args, **kwargs):
|
||||
""" function to implement in the subclass which sets up the columns """
|
||||
@@ -140,33 +128,13 @@ class ToasterTable(TemplateView):
|
||||
""" function to implement in the subclass which sets up the queryset"""
|
||||
pass
|
||||
|
||||
def add_filter(self, name, title, filter_actions):
|
||||
def add_filter(self, table_filter):
|
||||
"""Add a filter to the table.
|
||||
|
||||
Args:
|
||||
name (str): Unique identifier of the filter.
|
||||
title (str): Title of the filter.
|
||||
filter_actions: Actions for all the filters.
|
||||
table_filter: Filter instance
|
||||
"""
|
||||
self.filters[name] = {
|
||||
'title' : title,
|
||||
'filter_actions' : filter_actions,
|
||||
}
|
||||
|
||||
def make_filter_action(self, name, title, queryset_filter):
|
||||
"""
|
||||
Utility to make a filter_action; queryset_filter is an instance
|
||||
of QuerysetFilter or a function
|
||||
"""
|
||||
|
||||
action = {
|
||||
'title' : title,
|
||||
'name' : name,
|
||||
}
|
||||
|
||||
self.filter_actions[name] = queryset_filter
|
||||
|
||||
return action
|
||||
self.filter_map.add_filter(table_filter.name, table_filter)
|
||||
|
||||
def add_column(self, title="", help_text="",
|
||||
orderable=False, hideable=True, hidden=False,
|
||||
@@ -216,19 +184,41 @@ class ToasterTable(TemplateView):
|
||||
return template.render(context)
|
||||
|
||||
def apply_filter(self, filters, **kwargs):
|
||||
"""
|
||||
Apply a filter submitted in the querystring to the ToasterTable
|
||||
|
||||
filters: (str) in the format:
|
||||
'<filter name>:<action name>!<action params>'
|
||||
where <action params> is optional
|
||||
|
||||
<filter name> and <action name> are used to look up the correct filter
|
||||
in the ToasterTable's filter map; the <action params> are set on
|
||||
TableFilterAction* before its filter is applied and may modify the
|
||||
queryset returned by the filter
|
||||
"""
|
||||
self.setup_filters(**kwargs)
|
||||
|
||||
try:
|
||||
filter_name, filter_action = filters.split(':')
|
||||
filter_name, action_name_and_params = filters.split(':')
|
||||
|
||||
action_name = None
|
||||
action_params = None
|
||||
if re.search('!', action_name_and_params):
|
||||
action_name, action_params = action_name_and_params.split('!')
|
||||
action_params = urllib.unquote_plus(action_params)
|
||||
else:
|
||||
action_name = action_name_and_params
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if "all" in filter_action:
|
||||
if "all" in action_name:
|
||||
return
|
||||
|
||||
try:
|
||||
queryset_filter = self.filter_actions[filter_action]
|
||||
self.queryset = queryset_filter.filter(self.queryset)
|
||||
table_filter = self.filter_map.get_filter(filter_name)
|
||||
action = table_filter.get_action(action_name)
|
||||
action.set_params(action_params)
|
||||
self.queryset = action.filter(self.queryset)
|
||||
except KeyError:
|
||||
# pass it to the user - programming error here
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user