diff options
| -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 |
