diff options
author | Elliot Smith <elliot.smith@intel.com> | 2016-01-15 13:00:50 +0200 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2016-01-15 16:29:59 +0000 |
commit | 809046c6fbd544907b5d5f3bb554b71724c74661 (patch) | |
tree | 9a20e81100395ba67934f3e865c6e9356f4e0232 /bitbake/lib/toaster | |
parent | 294579b531d5a96a17aa863554e71f4680d35812 (diff) | |
download | poky-809046c6fbd544907b5d5f3bb554b71724c74661.tar.gz |
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>
Diffstat (limited to 'bitbake/lib/toaster')
-rw-r--r-- | bitbake/lib/toaster/toastergui/querysetfilter.py | 7 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/static/js/table.js | 80 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/tablefilter.py | 119 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/tables.py | 132 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/widgets.py | 90 |
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): | |||
5 | if criteria: | 5 | if criteria: |
6 | self.set_criteria(criteria) | 6 | self.set_criteria(criteria) |
7 | 7 | ||
8 | def set_criteria(self, criteria): | 8 | def set_criteria(self, criteria = None): |
9 | """ | 9 | """ |
10 | criteria is an instance of django.db.models.Q; | 10 | criteria is an instance of django.db.models.Q; |
11 | see https://docs.djangoproject.com/en/1.9/ref/models/querysets/#q-objects | 11 | see https://docs.djangoproject.com/en/1.9/ref/models/querysets/#q-objects |
@@ -17,7 +17,10 @@ class QuerysetFilter(object): | |||
17 | Filter queryset according to the criteria for this filter, | 17 | Filter queryset according to the criteria for this filter, |
18 | returning the filtered queryset | 18 | returning the filtered queryset |
19 | """ | 19 | """ |
20 | return queryset.filter(self.criteria) | 20 | if self.criteria: |
21 | return queryset.filter(self.criteria) | ||
22 | else: | ||
23 | return queryset | ||
21 | 24 | ||
22 | def count(self, queryset): | 25 | def count(self, queryset): |
23 | """ Returns a count of the elements in the filtered queryset """ | 26 | """ 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){ | |||
415 | data: params, | 415 | data: params, |
416 | headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, | 416 | headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, |
417 | success: function (filterData) { | 417 | success: function (filterData) { |
418 | var filterActionRadios = $('#filter-actions-'+ctx.tableName); | 418 | /* |
419 | filterData structure: | ||
420 | |||
421 | { | ||
422 | title: '<title for the filter popup>', | ||
423 | filter_actions: [ | ||
424 | { | ||
425 | title: '<label for radio button inside the popup>', | ||
426 | name: '<name of the filter action>', | ||
427 | count: <number of items this filter will show> | ||
428 | } | ||
429 | ] | ||
430 | } | ||
419 | 431 | ||
420 | $('#filter-modal-title-'+ctx.tableName).text(filterData.title); | 432 | each filter_action gets a radio button; the value of this is |
433 | set to filterName + ':' + filter_action.name; e.g. | ||
421 | 434 | ||
422 | filterActionRadios.text(""); | 435 | in_current_project:in_project |
423 | 436 | ||
424 | for (var i in filterData.filter_actions){ | 437 | specifies the "in_project" action of the "in_current_project" |
425 | var filterAction = filterData.filter_actions[i]; | 438 | filter |
426 | 439 | ||
427 | var action = $('<label class="radio"><input type="radio" name="filter" value=""><span class="filter-title"></span></label>'); | 440 | the filterName is set on the column filter icon, and corresponds |
428 | var actionTitle = filterAction.title + ' (' + filterAction.count + ')'; | 441 | to a value in the table's filters property |
429 | 442 | ||
430 | var radioInput = action.children("input"); | 443 | when the filter popup's "Apply" button is clicked, the |
444 | value for the radio button which is checked is passed in the | ||
445 | querystring and applied to the queryset on the table | ||
446 | */ | ||
431 | 447 | ||
432 | if (Number(filterAction.count) == 0){ | 448 | var filterActionRadios = $('#filter-actions-'+ctx.tableName); |
433 | radioInput.attr("disabled", "disabled"); | ||
434 | } | ||
435 | 449 | ||
436 | action.children(".filter-title").text(actionTitle); | 450 | $('#filter-modal-title-'+ctx.tableName).text(filterData.title); |
437 | 451 | ||
438 | radioInput.val(filterName + ':' + filterAction.name); | 452 | filterActionRadios.text(""); |
439 | 453 | ||
440 | /* Setup the current selected filter, default to 'all' if | 454 | for (var i in filterData.filter_actions) { |
441 | * no current filter selected. | 455 | var filterAction = filterData.filter_actions[i]; |
442 | */ | 456 | var action = null; |
443 | if ((tableParams.filter && | 457 | |
444 | tableParams.filter === radioInput.val()) || | 458 | if (filterAction.type === 'toggle') { |
445 | filterAction.name == 'all') { | 459 | var actionTitle = filterAction.title + ' (' + filterAction.count + ')'; |
446 | radioInput.attr("checked", "checked"); | 460 | |
461 | action = $('<label class="radio">' + | ||
462 | '<input type="radio" name="filter" value="">' + | ||
463 | '<span class="filter-title">' + | ||
464 | actionTitle + | ||
465 | '</span>' + | ||
466 | '</label>'); | ||
467 | |||
468 | var radioInput = action.children("input"); | ||
469 | if (Number(filterAction.count) == 0) { | ||
470 | radioInput.attr("disabled", "disabled"); | ||
471 | } | ||
472 | |||
473 | radioInput.val(filterData.name + ':' + filterAction.action_name); | ||
474 | |||
475 | /* Setup the current selected filter, default to 'all' if | ||
476 | * no current filter selected. | ||
477 | */ | ||
478 | if ((tableParams.filter && | ||
479 | tableParams.filter === radioInput.val()) || | ||
480 | filterAction.action_name == 'all') { | ||
481 | radioInput.attr("checked", "checked"); | ||
482 | } | ||
447 | } | 483 | } |
448 | 484 | ||
449 | filterActionRadios.append(action); | 485 | if (action) { |
486 | filterActionRadios.append(action); | ||
487 | } | ||
450 | } | 488 | } |
451 | 489 | ||
452 | $('#filter-modal-'+ctx.tableName).modal('show'); | 490 | $('#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 @@ | |||
1 | # | ||
2 | # ex:ts=4:sw=4:sts=4:et | ||
3 | # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- | ||
4 | # | ||
5 | # BitBake Toaster Implementation | ||
6 | # | ||
7 | # Copyright (C) 2015 Intel Corporation | ||
8 | # | ||
9 | # This program is free software; you can redistribute it and/or modify | ||
10 | # it under the terms of the GNU General Public License version 2 as | ||
11 | # published by the Free Software Foundation. | ||
12 | # | ||
13 | # This program is distributed in the hope that it will be useful, | ||
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
16 | # GNU General Public License for more details. | ||
17 | # | ||
18 | # You should have received a copy of the GNU General Public License along | ||
19 | # with this program; if not, write to the Free Software Foundation, Inc., | ||
20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
21 | |||
22 | class TableFilter(object): | ||
23 | """ | ||
24 | Stores a filter for a named field, and can retrieve the action | ||
25 | requested for that filter | ||
26 | """ | ||
27 | def __init__(self, name, title): | ||
28 | self.name = name | ||
29 | self.title = title | ||
30 | self.__filter_action_map = {} | ||
31 | |||
32 | def add_action(self, action): | ||
33 | self.__filter_action_map[action.name] = action | ||
34 | |||
35 | def get_action(self, action_name): | ||
36 | return self.__filter_action_map[action_name] | ||
37 | |||
38 | def to_json(self, queryset): | ||
39 | """ | ||
40 | Dump all filter actions as an object which can be JSON serialised; | ||
41 | this is used to generate the JSON for processing in | ||
42 | table.js / filterOpenClicked() | ||
43 | """ | ||
44 | filter_actions = [] | ||
45 | |||
46 | # add the "all" pseudo-filter action, which just selects the whole | ||
47 | # queryset | ||
48 | filter_actions.append({ | ||
49 | 'action_name' : 'all', | ||
50 | 'title' : 'All', | ||
51 | 'type': 'toggle', | ||
52 | 'count' : queryset.count() | ||
53 | }) | ||
54 | |||
55 | # add other filter actions | ||
56 | for action_name, filter_action in self.__filter_action_map.iteritems(): | ||
57 | obj = filter_action.to_json(queryset) | ||
58 | obj['action_name'] = action_name | ||
59 | filter_actions.append(obj) | ||
60 | |||
61 | return { | ||
62 | 'name': self.name, | ||
63 | 'title': self.title, | ||
64 | 'filter_actions': filter_actions | ||
65 | } | ||
66 | |||
67 | class TableFilterActionToggle(object): | ||
68 | """ | ||
69 | Stores a single filter action which will populate one radio button of | ||
70 | a ToasterTable filter popup; this filter can either be on or off and | ||
71 | has no other parameters | ||
72 | """ | ||
73 | |||
74 | def __init__(self, name, title, queryset_filter): | ||
75 | self.name = name | ||
76 | self.title = title | ||
77 | self.__queryset_filter = queryset_filter | ||
78 | self.type = 'toggle' | ||
79 | |||
80 | def set_params(self, params): | ||
81 | """ | ||
82 | params: (str) a string of extra parameters for the action; | ||
83 | the structure of this string depends on the type of action; | ||
84 | it's ignored for a toggle filter action, which is just on or off | ||
85 | """ | ||
86 | pass | ||
87 | |||
88 | def filter(self, queryset): | ||
89 | return self.__queryset_filter.filter(queryset) | ||
90 | |||
91 | def to_json(self, queryset): | ||
92 | """ Dump as a JSON object """ | ||
93 | return { | ||
94 | 'title': self.title, | ||
95 | 'type': self.type, | ||
96 | 'count': self.__queryset_filter.count(queryset) | ||
97 | } | ||
98 | |||
99 | class TableFilterMap(object): | ||
100 | """ | ||
101 | Map from field names to Filter objects for those fields | ||
102 | """ | ||
103 | def __init__(self): | ||
104 | self.__filters = {} | ||
105 | |||
106 | def add_filter(self, filter_name, table_filter): | ||
107 | """ table_filter is an instance of Filter """ | ||
108 | self.__filters[filter_name] = table_filter | ||
109 | |||
110 | def get_filter(self, filter_name): | ||
111 | return self.__filters[filter_name] | ||
112 | |||
113 | def to_json(self, queryset): | ||
114 | data = {} | ||
115 | |||
116 | for filter_name, table_filter in self.__filters.iteritems(): | ||
117 | data[filter_name] = table_filter.to_json() | ||
118 | |||
119 | 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 | |||
28 | from django.core.urlresolvers import reverse | 28 | from django.core.urlresolvers import reverse |
29 | from django.views.generic import TemplateView | 29 | from django.views.generic import TemplateView |
30 | 30 | ||
31 | from toastergui.tablefilter import TableFilter, TableFilterActionToggle | ||
32 | |||
31 | class ProjectFilters(object): | 33 | class ProjectFilters(object): |
32 | def __init__(self, project_layers): | 34 | def __init__(self, project_layers): |
33 | self.in_project = QuerysetFilter(Q(layer_version__in=project_layers)) | 35 | self.in_project = QuerysetFilter(Q(layer_version__in=project_layers)) |
@@ -53,16 +55,28 @@ class LayersTable(ToasterTable): | |||
53 | project = Project.objects.get(pk=kwargs['pid']) | 55 | project = Project.objects.get(pk=kwargs['pid']) |
54 | self.project_layers = ProjectLayer.objects.filter(project=project) | 56 | self.project_layers = ProjectLayer.objects.filter(project=project) |
55 | 57 | ||
58 | in_current_project_filter = TableFilter( | ||
59 | "in_current_project", | ||
60 | "Filter by project layers" | ||
61 | ) | ||
62 | |||
56 | criteria = Q(projectlayer__in=self.project_layers) | 63 | criteria = Q(projectlayer__in=self.project_layers) |
57 | in_project_filter = QuerysetFilter(criteria) | ||
58 | not_in_project_filter = QuerysetFilter(~criteria) | ||
59 | 64 | ||
60 | self.add_filter(title="Filter by project layers", | 65 | in_project_filter_action = TableFilterActionToggle( |
61 | name="in_current_project", | 66 | "in_project", |
62 | filter_actions=[ | 67 | "Layers added to this project", |
63 | self.make_filter_action("in_project", "Layers added to this project", in_project_filter), | 68 | QuerysetFilter(criteria) |
64 | self.make_filter_action("not_in_project", "Layers not added to this project", not_in_project_filter) | 69 | ) |
65 | ]) | 70 | |
71 | not_in_project_filter_action = TableFilterActionToggle( | ||
72 | "not_in_project", | ||
73 | "Layers not added to this project", | ||
74 | QuerysetFilter(~criteria) | ||
75 | ) | ||
76 | |||
77 | in_current_project_filter.add_action(in_project_filter_action) | ||
78 | in_current_project_filter.add_action(not_in_project_filter_action) | ||
79 | self.add_filter(in_current_project_filter) | ||
66 | 80 | ||
67 | def setup_queryset(self, *args, **kwargs): | 81 | def setup_queryset(self, *args, **kwargs): |
68 | prj = Project.objects.get(pk = kwargs['pid']) | 82 | prj = Project.objects.get(pk = kwargs['pid']) |
@@ -199,12 +213,26 @@ class MachinesTable(ToasterTable): | |||
199 | 213 | ||
200 | project_filters = ProjectFilters(self.project_layers) | 214 | project_filters = ProjectFilters(self.project_layers) |
201 | 215 | ||
202 | self.add_filter(title="Filter by project machines", | 216 | in_current_project_filter = TableFilter( |
203 | name="in_current_project", | 217 | "in_current_project", |
204 | filter_actions=[ | 218 | "Filter by project machines" |
205 | self.make_filter_action("in_project", "Machines provided by layers added to this project", project_filters.in_project), | 219 | ) |
206 | self.make_filter_action("not_in_project", "Machines provided by layers not added to this project", project_filters.not_in_project) | 220 | |
207 | ]) | 221 | in_project_filter_action = TableFilterActionToggle( |
222 | "in_project", | ||
223 | "Machines provided by layers added to this project", | ||
224 | project_filters.in_project | ||
225 | ) | ||
226 | |||
227 | not_in_project_filter_action = TableFilterActionToggle( | ||
228 | "not_in_project", | ||
229 | "Machines provided by layers not added to this project", | ||
230 | project_filters.not_in_project | ||
231 | ) | ||
232 | |||
233 | in_current_project_filter.add_action(in_project_filter_action) | ||
234 | in_current_project_filter.add_action(not_in_project_filter_action) | ||
235 | self.add_filter(in_current_project_filter) | ||
208 | 236 | ||
209 | def setup_queryset(self, *args, **kwargs): | 237 | def setup_queryset(self, *args, **kwargs): |
210 | prj = Project.objects.get(pk = kwargs['pid']) | 238 | prj = Project.objects.get(pk = kwargs['pid']) |
@@ -318,12 +346,26 @@ class RecipesTable(ToasterTable): | |||
318 | def setup_filters(self, *args, **kwargs): | 346 | def setup_filters(self, *args, **kwargs): |
319 | project_filters = ProjectFilters(self.project_layers) | 347 | project_filters = ProjectFilters(self.project_layers) |
320 | 348 | ||
321 | self.add_filter(title="Filter by project recipes", | 349 | table_filter = TableFilter( |
322 | name="in_current_project", | 350 | 'in_current_project', |
323 | filter_actions=[ | 351 | 'Filter by project recipes' |
324 | self.make_filter_action("in_project", "Recipes provided by layers added to this project", project_filters.in_project), | 352 | ) |
325 | self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", project_filters.not_in_project) | 353 | |
326 | ]) | 354 | in_project_filter_action = TableFilterActionToggle( |
355 | 'in_project', | ||
356 | 'Recipes provided by layers added to this project', | ||
357 | project_filters.in_project | ||
358 | ) | ||
359 | |||
360 | not_in_project_filter_action = TableFilterActionToggle( | ||
361 | 'not_in_project', | ||
362 | 'Recipes provided by layers not added to this project', | ||
363 | project_filters.not_in_project | ||
364 | ) | ||
365 | |||
366 | table_filter.add_action(in_project_filter_action) | ||
367 | table_filter.add_action(not_in_project_filter_action) | ||
368 | self.add_filter(table_filter) | ||
327 | 369 | ||
328 | def setup_queryset(self, *args, **kwargs): | 370 | def setup_queryset(self, *args, **kwargs): |
329 | prj = Project.objects.get(pk = kwargs['pid']) | 371 | prj = Project.objects.get(pk = kwargs['pid']) |
@@ -1070,47 +1112,47 @@ class BuildsTable(ToasterTable): | |||
1070 | 1112 | ||
1071 | def setup_filters(self, *args, **kwargs): | 1113 | def setup_filters(self, *args, **kwargs): |
1072 | # outcomes | 1114 | # outcomes |
1073 | filter_only_successful_builds = QuerysetFilter(Q(outcome=Build.SUCCEEDED)) | 1115 | outcome_filter = TableFilter( |
1074 | successful_builds_filter = self.make_filter_action( | 1116 | 'outcome_filter', |
1117 | 'Filter builds by outcome' | ||
1118 | ) | ||
1119 | |||
1120 | successful_builds_filter_action = TableFilterActionToggle( | ||
1075 | 'successful_builds', | 1121 | 'successful_builds', |
1076 | 'Successful builds', | 1122 | 'Successful builds', |
1077 | filter_only_successful_builds | 1123 | QuerysetFilter(Q(outcome=Build.SUCCEEDED)) |
1078 | ) | 1124 | ) |
1079 | 1125 | ||
1080 | filter_only_failed_builds = QuerysetFilter(Q(outcome=Build.FAILED)) | 1126 | failed_builds_filter_action = TableFilterActionToggle( |
1081 | failed_builds_filter = self.make_filter_action( | ||
1082 | 'failed_builds', | 1127 | 'failed_builds', |
1083 | 'Failed builds', | 1128 | 'Failed builds', |
1084 | filter_only_failed_builds | 1129 | QuerysetFilter(Q(outcome=Build.FAILED)) |
1085 | ) | 1130 | ) |
1086 | 1131 | ||
1087 | self.add_filter(title='Filter builds by outcome', | 1132 | outcome_filter.add_action(successful_builds_filter_action) |
1088 | name='outcome_filter', | 1133 | outcome_filter.add_action(failed_builds_filter_action) |
1089 | filter_actions = [ | 1134 | self.add_filter(outcome_filter) |
1090 | successful_builds_filter, | ||
1091 | failed_builds_filter | ||
1092 | ]) | ||
1093 | 1135 | ||
1094 | # failed tasks | 1136 | # failed tasks |
1137 | failed_tasks_filter = TableFilter( | ||
1138 | 'failed_tasks_filter', | ||
1139 | 'Filter builds by failed tasks' | ||
1140 | ) | ||
1141 | |||
1095 | criteria = Q(task_build__outcome=Task.OUTCOME_FAILED) | 1142 | criteria = Q(task_build__outcome=Task.OUTCOME_FAILED) |
1096 | filter_only_builds_with_failed_tasks = QuerysetFilter(criteria) | 1143 | |
1097 | with_failed_tasks_filter = self.make_filter_action( | 1144 | with_failed_tasks_filter_action = TableFilterActionToggle( |
1098 | 'with_failed_tasks', | 1145 | 'with_failed_tasks', |
1099 | 'Builds with failed tasks', | 1146 | 'Builds with failed tasks', |
1100 | filter_only_builds_with_failed_tasks | 1147 | QuerysetFilter(criteria) |
1101 | ) | 1148 | ) |
1102 | 1149 | ||
1103 | criteria = ~Q(task_build__outcome=Task.OUTCOME_FAILED) | 1150 | without_failed_tasks_filter_action = TableFilterActionToggle( |
1104 | filter_only_builds_without_failed_tasks = QuerysetFilter(criteria) | ||
1105 | without_failed_tasks_filter = self.make_filter_action( | ||
1106 | 'without_failed_tasks', | 1151 | 'without_failed_tasks', |
1107 | 'Builds without failed tasks', | 1152 | 'Builds without failed tasks', |
1108 | filter_only_builds_without_failed_tasks | 1153 | QuerysetFilter(~criteria) |
1109 | ) | 1154 | ) |
1110 | 1155 | ||
1111 | self.add_filter(title='Filter builds by failed tasks', | 1156 | failed_tasks_filter.add_action(with_failed_tasks_filter_action) |
1112 | name='failed_tasks_filter', | 1157 | failed_tasks_filter.add_action(without_failed_tasks_filter_action) |
1113 | filter_actions = [ | 1158 | self.add_filter(failed_tasks_filter) |
1114 | with_failed_tasks_filter, | ||
1115 | without_failed_tasks_filter | ||
1116 | ]) | ||
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 | |||
39 | import collections | 39 | import collections |
40 | import operator | 40 | import operator |
41 | import re | 41 | import re |
42 | import urllib | ||
42 | 43 | ||
43 | import logging | 44 | import logging |
44 | logger = logging.getLogger("toaster") | 45 | logger = logging.getLogger("toaster") |
45 | 46 | ||
46 | from toastergui.views import objtojson | 47 | from toastergui.views import objtojson |
48 | from toastergui.tablefilter import TableFilterMap | ||
47 | 49 | ||
48 | class ToasterTable(TemplateView): | 50 | class ToasterTable(TemplateView): |
49 | def __init__(self, *args, **kwargs): | 51 | def __init__(self, *args, **kwargs): |
@@ -53,7 +55,10 @@ class ToasterTable(TemplateView): | |||
53 | self.title = "Table" | 55 | self.title = "Table" |
54 | self.queryset = None | 56 | self.queryset = None |
55 | self.columns = [] | 57 | self.columns = [] |
56 | self.filters = {} | 58 | |
59 | # map from field names to Filter instances | ||
60 | self.filter_map = TableFilterMap() | ||
61 | |||
57 | self.total_count = 0 | 62 | self.total_count = 0 |
58 | self.static_context_extra = {} | 63 | self.static_context_extra = {} |
59 | self.filter_actions = {} | 64 | self.filter_actions = {} |
@@ -66,7 +71,7 @@ class ToasterTable(TemplateView): | |||
66 | orderable=True, | 71 | orderable=True, |
67 | field_name="id") | 72 | field_name="id") |
68 | 73 | ||
69 | # prevent HTTP caching of table data | 74 | # prevent HTTP caching of table data |
70 | @cache_control(must_revalidate=True, max_age=0, no_store=True, no_cache=True) | 75 | @cache_control(must_revalidate=True, max_age=0, no_store=True, no_cache=True) |
71 | def dispatch(self, *args, **kwargs): | 76 | def dispatch(self, *args, **kwargs): |
72 | return super(ToasterTable, self).dispatch(*args, **kwargs) | 77 | return super(ToasterTable, self).dispatch(*args, **kwargs) |
@@ -108,27 +113,10 @@ class ToasterTable(TemplateView): | |||
108 | self.apply_search(search) | 113 | self.apply_search(search) |
109 | 114 | ||
110 | name = request.GET.get("name", None) | 115 | name = request.GET.get("name", None) |
111 | if name is None: | 116 | table_filter = self.filter_map.get_filter(name) |
112 | data = json.dumps(self.filters, | 117 | return json.dumps(table_filter.to_json(self.queryset), |
113 | indent=2, | 118 | indent=2, |
114 | cls=DjangoJSONEncoder) | 119 | cls=DjangoJSONEncoder) |
115 | else: | ||
116 | for actions in self.filters[name]['filter_actions']: | ||
117 | queryset_filter = self.filter_actions[actions['name']] | ||
118 | actions['count'] = queryset_filter.count(self.queryset) | ||
119 | |||
120 | # Add the "All" items filter action | ||
121 | self.filters[name]['filter_actions'].insert(0, { | ||
122 | 'name' : 'all', | ||
123 | 'title' : 'All', | ||
124 | 'count' : self.queryset.count(), | ||
125 | }) | ||
126 | |||
127 | data = json.dumps(self.filters[name], | ||
128 | indent=2, | ||
129 | cls=DjangoJSONEncoder) | ||
130 | |||
131 | return data | ||
132 | 120 | ||
133 | def setup_columns(self, *args, **kwargs): | 121 | def setup_columns(self, *args, **kwargs): |
134 | """ function to implement in the subclass which sets up the columns """ | 122 | """ function to implement in the subclass which sets up the columns """ |
@@ -140,33 +128,13 @@ class ToasterTable(TemplateView): | |||
140 | """ function to implement in the subclass which sets up the queryset""" | 128 | """ function to implement in the subclass which sets up the queryset""" |
141 | pass | 129 | pass |
142 | 130 | ||
143 | def add_filter(self, name, title, filter_actions): | 131 | def add_filter(self, table_filter): |
144 | """Add a filter to the table. | 132 | """Add a filter to the table. |
145 | 133 | ||
146 | Args: | 134 | Args: |
147 | name (str): Unique identifier of the filter. | 135 | table_filter: Filter instance |
148 | title (str): Title of the filter. | ||
149 | filter_actions: Actions for all the filters. | ||
150 | """ | 136 | """ |
151 | self.filters[name] = { | 137 | self.filter_map.add_filter(table_filter.name, table_filter) |
152 | 'title' : title, | ||
153 | 'filter_actions' : filter_actions, | ||
154 | } | ||
155 | |||
156 | def make_filter_action(self, name, title, queryset_filter): | ||
157 | """ | ||
158 | Utility to make a filter_action; queryset_filter is an instance | ||
159 | of QuerysetFilter or a function | ||
160 | """ | ||
161 | |||
162 | action = { | ||
163 | 'title' : title, | ||
164 | 'name' : name, | ||
165 | } | ||
166 | |||
167 | self.filter_actions[name] = queryset_filter | ||
168 | |||
169 | return action | ||
170 | 138 | ||
171 | def add_column(self, title="", help_text="", | 139 | def add_column(self, title="", help_text="", |
172 | orderable=False, hideable=True, hidden=False, | 140 | orderable=False, hideable=True, hidden=False, |
@@ -216,19 +184,41 @@ class ToasterTable(TemplateView): | |||
216 | return template.render(context) | 184 | return template.render(context) |
217 | 185 | ||
218 | def apply_filter(self, filters, **kwargs): | 186 | def apply_filter(self, filters, **kwargs): |
187 | """ | ||
188 | Apply a filter submitted in the querystring to the ToasterTable | ||
189 | |||
190 | filters: (str) in the format: | ||
191 | '<filter name>:<action name>!<action params>' | ||
192 | where <action params> is optional | ||
193 | |||
194 | <filter name> and <action name> are used to look up the correct filter | ||
195 | in the ToasterTable's filter map; the <action params> are set on | ||
196 | TableFilterAction* before its filter is applied and may modify the | ||
197 | queryset returned by the filter | ||
198 | """ | ||
219 | self.setup_filters(**kwargs) | 199 | self.setup_filters(**kwargs) |
220 | 200 | ||
221 | try: | 201 | try: |
222 | filter_name, filter_action = filters.split(':') | 202 | filter_name, action_name_and_params = filters.split(':') |
203 | |||
204 | action_name = None | ||
205 | action_params = None | ||
206 | if re.search('!', action_name_and_params): | ||
207 | action_name, action_params = action_name_and_params.split('!') | ||
208 | action_params = urllib.unquote_plus(action_params) | ||
209 | else: | ||
210 | action_name = action_name_and_params | ||
223 | except ValueError: | 211 | except ValueError: |
224 | return | 212 | return |
225 | 213 | ||
226 | if "all" in filter_action: | 214 | if "all" in action_name: |
227 | return | 215 | return |
228 | 216 | ||
229 | try: | 217 | try: |
230 | queryset_filter = self.filter_actions[filter_action] | 218 | table_filter = self.filter_map.get_filter(filter_name) |
231 | self.queryset = queryset_filter.filter(self.queryset) | 219 | action = table_filter.get_action(action_name) |
220 | action.set_params(action_params) | ||
221 | self.queryset = action.filter(self.queryset) | ||
232 | except KeyError: | 222 | except KeyError: |
233 | # pass it to the user - programming error here | 223 | # pass it to the user - programming error here |
234 | raise | 224 | raise |