summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorElliot Smith <elliot.smith@intel.com>2016-01-15 13:00:50 +0200
committerRichard Purdie <richard.purdie@linuxfoundation.org>2016-01-15 16:29:59 +0000
commit809046c6fbd544907b5d5f3bb554b71724c74661 (patch)
tree9a20e81100395ba67934f3e865c6e9356f4e0232
parent294579b531d5a96a17aa863554e71f4680d35812 (diff)
downloadpoky-809046c6fbd544907b5d5f3bb554b71724c74661.tar.gz
poky-809046c6fbd544907b5d5f3bb554b71724c74661.tar.bz2
poky-809046c6fbd544907b5d5f3bb554b71724c74661.zip
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>
-rw-r--r--bitbake/lib/toaster/toastergui/querysetfilter.py7
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/table.js80
-rw-r--r--bitbake/lib/toaster/toastergui/tablefilter.py119
-rw-r--r--bitbake/lib/toaster/toastergui/tables.py132
-rw-r--r--bitbake/lib/toaster/toastergui/widgets.py90
5 files changed, 310 insertions, 118 deletions
diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py b/bitbake/lib/toaster/toastergui/querysetfilter.py
index 62297e9b89..dbae239370 100644
--- a/bitbake/lib/toaster/toastergui/querysetfilter.py
+++ b/bitbake/lib/toaster/toastergui/querysetfilter.py
@@ -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 """
diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index c69c205d50..fa01ddf47e 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -415,38 +415,76 @@ function tableInit(ctx){
data: params,
headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
success: function (filterData) {
- var filterActionRadios = $('#filter-actions-'+ctx.tableName);
+ /*
+ 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>
+ }
+ ]
+ }
- $('#filter-modal-title-'+ctx.tableName).text(filterData.title);
+ each filter_action gets a radio button; the value of this is
+ set to filterName + ':' + filter_action.name; e.g.
- filterActionRadios.text("");
+ in_current_project:in_project
- for (var i in filterData.filter_actions){
- var filterAction = filterData.filter_actions[i];
+ specifies the "in_project" action of the "in_current_project"
+ filter
- var action = $('<label class="radio"><input type="radio" name="filter" value=""><span class="filter-title"></span></label>');
- var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
+ the filterName is set on the column filter icon, and corresponds
+ to a value in the table's filters property
- var radioInput = action.children("input");
+ 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
+ */
- if (Number(filterAction.count) == 0){
- radioInput.attr("disabled", "disabled");
- }
+ var filterActionRadios = $('#filter-actions-'+ctx.tableName);
- action.children(".filter-title").text(actionTitle);
+ $('#filter-modal-title-'+ctx.tableName).text(filterData.title);
- radioInput.val(filterName + ':' + filterAction.name);
+ filterActionRadios.text("");
- /* 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");
+ for (var i in filterData.filter_actions) {
+ var filterAction = filterData.filter_actions[i];
+ var action = null;
+
+ if (filterAction.type === 'toggle') {
+ var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
+
+ action = $('<label class="radio">' +
+ '<input type="radio" name="filter" value="">' +
+ '<span class="filter-title">' +
+ actionTitle +
+ '</span>' +
+ '</label>');
+
+ 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");
+ }
}
- filterActionRadios.append(action);
+ if (action) {
+ filterActionRadios.append(action);
+ }
}
$('#filter-modal-'+ctx.tableName).modal('show');
diff --git a/bitbake/lib/toaster/toastergui/tablefilter.py b/bitbake/lib/toaster/toastergui/tablefilter.py
new file mode 100644
index 0000000000..b42fd52865
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/tablefilter.py
@@ -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
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 116cff3f43..a0991ec3ea 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -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)
+ in_current_project_filter = TableFilter(
+ "in_current_project",
+ "Filter by project layers"
+ )
+
criteria = Q(projectlayer__in=self.project_layers)
- in_project_filter = QuerysetFilter(criteria)
- not_in_project_filter = QuerysetFilter(~criteria)
- 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)
- ])
+ 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)
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index 71b29eaa1e..8790340db9 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -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