summaryrefslogtreecommitdiffstats
path: root/bitbake
diff options
context:
space:
mode:
authorMichael Wood <michael.g.wood@intel.com>2015-05-08 17:24:11 +0100
committerRichard Purdie <richard.purdie@linuxfoundation.org>2015-05-14 18:04:09 +0100
commit7f8c44771cc6219ad7e58da7840fffbe93f11b39 (patch)
tree5c375661fec72a7d29c8cef6cc4a316d290ef5e1 /bitbake
parent2ac26e4294a029075071a8c6cfd5c2bdea801337 (diff)
downloadpoky-7f8c44771cc6219ad7e58da7840fffbe93f11b39.tar.gz
bitbake: toaster: Add toaster table widget
This widget provides a common client and backend widget to support presenting data tables in Toaster. It provides; data loading, paging, page size, ordering, filtering, column toggling, caching, column defaults, counts and search. (Bitbake rev: b3a6fa4861bf4495fbd39e2abb18b3a46c6eac18) Signed-off-by: Michael Wood <michael.g.wood@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake')
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/table.js485
-rw-r--r--bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html25
-rw-r--r--bitbake/lib/toaster/toastergui/templates/toastertable.html117
-rw-r--r--bitbake/lib/toaster/toastergui/widgets.py316
4 files changed, 943 insertions, 0 deletions
diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
new file mode 100644
index 0000000000..8f5bb2ebe1
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -0,0 +1,485 @@
1'use strict';
2
3function tableInit(ctx){
4
5 if (ctx.url.length === 0) {
6 throw "No url supplied for retreiving data";
7 }
8
9 var tableChromeDone = false;
10 var tableTotal = 0;
11
12 var tableParams = {
13 limit : 25,
14 page : 1,
15 orderby : null,
16 filter : null,
17 search : null,
18 };
19
20 var defaultHiddenCols = [];
21
22 var table = $("#" + ctx.tableName);
23
24 /* if we're loading clean from a url use it's parameters as the default */
25 var urlParams = libtoaster.parseUrlParams();
26
27 /* Merge the tableParams and urlParams object properties */
28 tableParams = $.extend(tableParams, urlParams);
29
30 /* Now fix the types that .extend changed for us */
31 tableParams.limit = Number(tableParams.limit);
32 tableParams.page = Number(tableParams.page);
33
34 loadData(tableParams);
35
36 window.onpopstate = function(event){
37 if (event.state){
38 tableParams = event.state.tableParams;
39 /* We skip loadData and just update the table */
40 updateTable(event.state.tableData);
41 }
42 };
43
44 function loadData(tableParams){
45 $.ajax({
46 type: "GET",
47 url: ctx.url,
48 data: tableParams,
49 headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
50 success: function(tableData) {
51 updateTable(tableData);
52 window.history.pushState({
53 tableData: tableData,
54 tableParams: tableParams
55 }, null, libtoaster.dumpsUrlParams(tableParams));
56 },
57
58 error: function (_data) {
59 console.warn("Call failed");
60 console.warn(_data);
61 }
62 });
63 }
64
65 function updateTable(tableData) {
66 var tableBody = table.children("tbody");
67 var paginationBtns = $('#pagination-'+ctx.tableName);
68
69 /* To avoid page re-layout flicker when paging set fixed height */
70 table.css("visibility", "hidden");
71 table.css("padding-bottom", table.height());
72
73 /* Reset table components */
74 tableBody.html("");
75 paginationBtns.html("");
76
77 if (tableParams.search)
78 $('.remove-search-btn-'+ctx.tableName).show();
79 else
80 $('.remove-search-btn-'+ctx.tableName).hide();
81
82 $('.table-count-' + ctx.tableName).text(tableData.total);
83 tableTotal = tableData.total;
84
85 if (tableData.total === 0){
86 $("#table-container-"+ctx.tableName).hide();
87 $("#new-search-input-"+ctx.tableName).val(tableParams.search);
88 $("#no-results-"+ctx.tableName).show();
89 return;
90 } else {
91 $("#table-container-"+ctx.tableName).show();
92 $("#no-results-"+ctx.tableName).hide();
93 }
94
95
96 setupTableChrome(tableData);
97
98
99 /* Add table data rows */
100 for (var i in tableData.rows){
101 var row = $("<tr></tr>");
102 for (var key_j in tableData.rows[i]){
103 var td = $("<td></td>");
104 td.prop("class", key_j);
105 if (tableData.rows[i][key_j]){
106 td.html(tableData.rows[i][key_j]);
107 }
108 row.append(td);
109 }
110 tableBody.append(row);
111
112 /* If we have layerbtns then initialise them */
113 layerBtnsInit(ctx);
114
115 /* If we have popovers initialise them now */
116 $('td > a.btn').popover({
117 html:true,
118 placement:'left',
119 container:'body',
120 trigger:'manual'
121 }).click(function(e){
122 $('td > a.btn').not(this).popover('hide');
123 /* ideally we would use 'toggle' here
124 * but it seems buggy in our Bootstrap version
125 */
126 $(this).popover('show');
127 e.stopPropagation();
128 });
129
130 /* enable help information tooltip */
131 $(".get-help").tooltip({container:'body', html:true, delay:{show:300}});
132 }
133
134 /* Setup the pagination controls */
135
136 var start = tableParams.page - 2;
137 var end = tableParams.page + 2;
138 var numPages = Math.ceil(tableData.total/tableParams.limit);
139
140 if (tableParams.page < 3)
141 end = 5;
142
143 for (var page_i=1; page_i <= numPages; page_i++){
144 if (page_i >= start && page_i <= end){
145 var btn = $('<li><a href="#" class="page">'+page_i+'</a></li>');
146
147 if (page_i === tableParams.page){
148 btn.addClass("active");
149 }
150
151 /* Add the click handler */
152 btn.click(pageButtonClicked);
153 paginationBtns.append(btn);
154 }
155 }
156 table.css("padding-bottom", 0);
157 loadColumnsPreference();
158
159 $("table").css("visibility", "visible");
160 }
161
162 function setupTableChrome(tableData){
163 if (tableChromeDone === true)
164 return;
165
166 var tableHeadRow = table.find("thead tr");
167 var editColMenu = $("#table-chrome-"+ctx.tableName).find(".editcol");
168
169 tableHeadRow.html("");
170 editColMenu.html("");
171
172 if (!tableParams.orderby && tableData.default_orderby){
173 tableParams.orderby = tableData.default_orderby;
174 }
175
176 /* Add table header and column toggle menu */
177 for (var i in tableData.columns){
178 var col = tableData.columns[i];
179 var header = $("<th></th>");
180 header.prop("class", col.field_name);
181
182 /* Setup the help text */
183 if (col.help_text.length > 0) {
184 var help_text = $('<i class="icon-question-sign get-help"> </i>');
185 help_text.tooltip({title: col.help_text});
186 header.append(help_text);
187 }
188
189 /* Setup the orderable title */
190 if (col.orderable) {
191 var title = $('<a href=\"#\" ></a>');
192
193 title.data('field-name', col.field_name);
194 title.text(col.title);
195 title.click(sortColumnClicked);
196
197 header.append(title);
198
199 header.append(' <i class="icon-caret-down" style="display:none"></i>');
200 header.append(' <i class="icon-caret-up" style="display:none"></i>');
201
202 /* If we're currently ordered setup the visual indicator */
203 if (col.field_name === tableParams.orderby ||
204 '-' + col.field_name === tableParams.orderby){
205 header.children("a").addClass("sorted");
206
207 if (tableParams.orderby.indexOf("-") === -1){
208 header.find('.icon-caret-down').show();
209 } else {
210 header.find('.icon-caret-up').show();
211 }
212 }
213
214 } else {
215 /* Not orderable */
216 header.addClass("muted");
217 header.css("font-weight", "normal");
218 header.append(col.title+' ');
219 }
220
221 /* Setup the filter button */
222 if (col.filter_name){
223 var filterBtn = $('<a href="#" role="button" class="pull-right btn btn-mini" data-toggle="modal"><i class="icon-filter filtered"></i></a>');
224
225 filterBtn.data('filter-name', col.filter_name);
226 filterBtn.click(filterOpenClicked);
227
228 /* If we're currently being filtered setup the visial indicator */
229 if (tableParams.filter &&
230 tableParams.filter.match('^'+col.filter_name)) {
231
232 filterBtn.addClass("btn-primary");
233
234 filterBtn.tooltip({
235 html: true,
236 title: '<button class="btn btn-small btn-primary" onClick=\'$("#clear-filter-btn").click();\'>Clear filter</button>',
237 placement: 'bottom',
238 delay: {
239 hide: 1500,
240 show: 400,
241 },
242 });
243 }
244 header.append(filterBtn);
245 }
246
247 /* Done making the header now add it */
248 tableHeadRow.append(header);
249
250 /* Now setup the checkbox state and click handler */
251 var toggler = $('<li><label class="checkbox">'+col.title+'<input type="checkbox" id="checkbox-'+ col.field_name +'" class="col-toggle" value="'+col.field_name+'" /></label></li>');
252
253 var togglerInput = toggler.find("input");
254
255 togglerInput.attr("checked","checked");
256
257 /* If we can hide the column enable the checkbox action */
258 if (col.hideable){
259 togglerInput.click(colToggleClicked);
260 } else {
261 toggler.find("label").addClass("muted");
262 togglerInput.attr("disabled", "disabled");
263 }
264
265 if (col.hidden) {
266 defaultHiddenCols.push(col.field_name);
267 }
268
269 editColMenu.append(toggler);
270 } /* End for each column */
271
272 tableChromeDone = true;
273 }
274
275 /* Display or hide table columns based on the cookie preference or defaults */
276 function loadColumnsPreference(){
277 var cookie_data = $.cookie("cols");
278
279 if (cookie_data) {
280 var cols_hidden = JSON.parse($.cookie("cols"));
281
282 /* For each of the columns check if we should hide them
283 * also update the checked status in the Edit columns menu
284 */
285 $("#"+ctx.tableName+" th").each(function(){
286 for (var i in cols_hidden){
287 if ($(this).hasClass(cols_hidden[i])){
288 $("."+cols_hidden[i]).hide();
289 $("#checkbox-"+cols_hidden[i]).removeAttr("checked");
290 }
291 }
292 });
293 } else {
294 /* Disable these columns by default when we have no columns
295 * user setting.
296 */
297 for (var i in defaultHiddenCols) {
298 $("."+defaultHiddenCols[i]).hide();
299 $("#checkbox-"+defaultHiddenCols[i]).removeAttr("checked");
300 }
301 }
302 }
303
304 function sortColumnClicked(){
305
306 /* We only have one sort at a time so remove any existing sort indicators */
307 $("#"+ctx.tableName+" th .icon-caret-down").hide();
308 $("#"+ctx.tableName+" th .icon-caret-up").hide();
309 $("#"+ctx.tableName+" th a").removeClass("sorted");
310
311 var fieldName = $(this).data('field-name');
312
313 /* if we're already sorted sort the other way */
314 if (tableParams.orderby === fieldName &&
315 tableParams.orderby.indexOf('-') === -1) {
316 tableParams.orderby = '-' + $(this).data('field-name');
317 $(this).parent().children('.icon-caret-up').show();
318 } else {
319 tableParams.orderby = $(this).data('field-name');
320 $(this).parent().children('.icon-caret-down').show();
321 }
322
323 $(this).addClass("sorted");
324
325 loadData(tableParams);
326 }
327
328 function pageButtonClicked(e) {
329 tableParams.page = Number($(this).text());
330 loadData(tableParams);
331 /* Stop page jumps when clicking on # links */
332 e.preventDefault();
333 }
334
335 /* Toggle a table column */
336 function colToggleClicked (){
337 var col = $(this).val();
338 var disabled_cols = [];
339
340 if ($(this).prop("checked")) {
341 $("."+col).show();
342 } else {
343 $("."+col).hide();
344 /* If we're ordered by the column we're hiding remove the order by */
345 if (col === tableParams.orderby ||
346 '-' + col === tableParams.orderby){
347 tableParams.orderby = null;
348 loadData(tableParams);
349 }
350 }
351
352 /* Update the cookie with the unchecked columns */
353 $(".col-toggle").not(":checked").map(function(){
354 disabled_cols.push($(this).val());
355 });
356
357 $.cookie("cols", JSON.stringify(disabled_cols));
358 }
359
360 function filterOpenClicked(){
361 var filterName = $(this).data('filter-name');
362
363 /* We need to pass in the curren search so that the filter counts take
364 * into account the current search filter
365 */
366 var params = {
367 'name' : filterName,
368 'search': tableParams.search
369 };
370
371 $.ajax({
372 type: "GET",
373 url: ctx.url + 'filterinfo',
374 data: params,
375 headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
376 success: function (filterData) {
377 var filterActionRadios = $('#filter-actions');
378
379 $('#filter-modal-title').text(filterData.title);
380
381 filterActionRadios.text("");
382
383 for (var i in filterData.filter_actions){
384 var filterAction = filterData.filter_actions[i];
385
386 var action = $('<label class="radio"><input type="radio" name="filter" value=""><span class="filter-title"></span></label>');
387 var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
388
389 var radioInput = action.children("input");
390
391 action.children(".filter-title").text(actionTitle);
392
393 radioInput.val(filterName + ':' + filterAction.name);
394
395 /* Setup the current selected filter, default to 'all' if
396 * no current filter selected.
397 */
398 if ((tableParams.filter &&
399 tableParams.filter === radioInput.val()) ||
400 filterAction.name == 'all') {
401 radioInput.attr("checked", "checked");
402 }
403
404 filterActionRadios.append(action);
405 }
406
407 $('#filter-modal').modal('show');
408 }
409 });
410 }
411
412
413 $(".get-help").tooltip({container:'body', html:true, delay:{show:300}});
414
415 /* Keep the Edit columns menu open after click by eating the event */
416 $('.dropdown-menu').click(function(e) {
417 e.stopPropagation();
418 });
419
420 $(".pagesize").val(tableParams.limit);
421
422 /* page size selector */
423 $(".pagesize").change(function(){
424 tableParams.limit = Number(this.value);
425 if ((tableParams.page * tableParams.limit) > tableTotal)
426 tableParams.page = 1;
427
428 loadData(tableParams);
429 /* sync the other selectors on the page */
430 $(".pagesize").val(this.value);
431 });
432
433 $("#search-submit-"+ctx.tableName).click(function(e){
434 var searchTerm = $("#search-input-"+ctx.tableName).val();
435
436 tableParams.page = 1;
437 tableParams.search = searchTerm;
438 tableParams.filter = null;
439
440 loadData(tableParams);
441
442 e.preventDefault();
443 });
444
445 $('.remove-search-btn-'+ctx.tableName).click(function(e){
446 e.preventDefault();
447
448 tableParams.page = 1;
449 tableParams.search = null;
450 loadData(tableParams);
451
452 $("#search-input-"+ctx.tableName).val("");
453 $(this).hide();
454 });
455
456 $("#search-input-"+ctx.tableName).keyup(function(e){
457 if (e.which === 13)
458 $('#search-submit-'+ctx.tableName).click();
459 });
460
461 /* Stop page jumps when clicking on # links */
462 $('a[href="#"]').click(function(e){
463 e.preventDefault();
464 });
465
466 $("#clear-filter-btn").click(function(){
467 tableParams.filter = null;
468 loadData(tableParams);
469 });
470
471 $("#filter-modal-form").submit(function(e){
472 e.preventDefault();
473
474 tableParams.filter = $(this).find("input[type='radio']:checked").val();
475
476 /* All === remove filter */
477 if (tableParams.filter.match(":all$"))
478 tableParams.filter = null;
479
480 loadData(tableParams);
481
482
483 $('#filter-modal').modal('hide');
484 });
485}
diff --git a/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html b/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html
new file mode 100644
index 0000000000..d7ad2e7eee
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html
@@ -0,0 +1,25 @@
1{% extends "baseprojectpage.html" %}
2{% load projecttags %}
3{% load humanize %}
4{% load static %}
5
6{% block localbreadcrumb %}
7
8
9<li>{{title}}</li>{% endblock %}
10
11{% block projectinfomain %}
12<div class="page-header">
13 <h1>{{title}} (<span class="table-count-{{table_name}}"></span>)
14 <i class="icon-question-sign get-help heading-help" title="This page lists {{title}} compatible with the release selected for this project, which is {{project.release.description}}"></i>
15 </h1>
16</div>
17<div id="zone1alerts" style="display:none">
18 <div class="alert alert-info lead">
19 <button type="button" class="close" id="hide-alert">&times;</button>
20 <span id="alert-msg"></span>
21 </div>
22</div>
23{% url table_name project.id as xhr_table_url %}
24{% include "toastertable.html" %}
25{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/toastertable.html b/bitbake/lib/toaster/toastergui/templates/toastertable.html
new file mode 100644
index 0000000000..05ec3662b2
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/toastertable.html
@@ -0,0 +1,117 @@
1
2{% load static %}
3{% load projecttags %}
4
5<script src="{% static 'js/table.js' %}"></script>
6<script src="{% static 'js/layerBtn.js' %}"></script>
7<script>
8 $(document).ready(function() {
9 (function(){
10
11 var ctx = {
12 tableName : "{{table_name}}",
13 url : "{{ xhr_table_url }}",
14 title : "{{title}}",
15 projectLayers : {{projectlayers|json}},
16 };
17
18 try {
19 tableInit(ctx);
20 } catch (e) {
21 document.write("Problem loading table widget: " + e);
22 }
23 })();
24 });
25</script>
26
27<!-- filter modal -->
28<div id="filter-modal" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="false">
29 <form id="filter-modal-form" style="margin-bottom: 0px">
30 <div class="modal-header">
31 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
32 <h3 id="filter-modal-title"></h3>
33 </div>
34 <div class="modal-body">
35 <p>Show:</p>
36 <span id="filter-actions"></span>
37 </div>
38 <div class="modal-footer">
39 <button class="btn btn-primary" type="submit">Apply</button>
40 </div>
41 </form>
42</div>
43<button id="clear-filter-btn" style="display:none"></button>
44
45<div class="row-fluid alert" id="no-results-{{table_name}}" style="display:none">
46 <form class="no-results input-append">
47 <input class="input-xxlarge" id="new-search-input-{{table_name}}" name="search" type="text" placeholder="Search {{title|lower}}" value="{{request.GET.search}}"/>
48 <a href="#" class="add-on btn remove-search-btn-{{table_name}}" tabindex="-1">
49 <i class="icon-remove"></i>
50 </a>
51 <button class="btn search-submit-{{table_name}}" >Search</button>
52 <button class="btn btn-link remove-search-btn-{{table_name}}">Show {{title|lower}}
53 </button>
54 </form>
55</div>
56
57<div id="table-container-{{table_name}}">
58 <!-- control header -->
59 <div class="navbar" id="table-chrome-{{table_name}}">
60 <div class="navbar-inner">
61 <div class="navbar-search input-append pull-left">
62
63 <input class="input-xxlarge" id="search-input-{{table_name}}" name="search" type="text" placeholder="Search {{title|lower}}" value="{{request.GET.search}}"/>
64 <a href="#" style="display:none" class="add-on btn remove-search-btn-{{table_name}}" tabindex="-1">
65 <i class="icon-remove"></i>
66 </a>
67 <button class="btn" id="search-submit-{{table_name}}" >Search</button>
68 </div>
69
70 <div class="pull-right">
71 <div class="btn-group">
72 <button class="btn dropdown-toggle" data-toggle="dropdown">Edit columns
73 <span class="caret"></span>
74 </button>
75 <ul class="dropdown-menu editcol">
76 </ul>
77 </div>
78 <div style="display:inline">
79 <span class="divider-vertical"></span>
80 <span class="help-inline" style="padding-top:5px;">Show rows:</span>
81 <select style="margin-top:5px;margin-bottom:0px;" class="pagesize">
82 {% with "10 25 50 100 150" as list%}
83 {% for i in list.split %}
84 <option value="{{i}}">{{i}}</option>
85 {% endfor %}
86 {% endwith %}
87 </select>
88 </div>
89 </div>
90 </div>
91 </div>
92
93 <!-- The actual table -->
94 <table class="table table-bordered table-hover tablesorter" id="{{table_name}}">
95 <thead>
96 <tr></tr>
97 </thead>
98 <tbody></tbody>
99 </table>
100
101 <!-- Pagination controls -->
102 <div class="pagination pagination-centered">
103 <ul id="pagination-{{table_name}}" class="pagination" style="display: block-inline">
104 </ul>
105
106 <div class="pull-right">
107 <span class="help-inline" style="padding-top:5px;">Show rows:</span>
108 <select style="margin-top:5px;margin-bottom:0px;" class="pagesize">
109 {% with "10 25 50 100 150" as list%}
110 {% for i in list.split %}
111 <option value="{{i}}">{{i}}</option>
112 {% endfor %}
113 {% endwith %}
114 </select>
115 </div>
116 </div>
117</div>
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
new file mode 100644
index 0000000000..8d449193af
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -0,0 +1,316 @@
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
22from django.views.generic import View, TemplateView
23from django.shortcuts import HttpResponse
24from django.http import HttpResponseBadRequest
25from django.core import serializers
26from django.core.cache import cache
27from django.core.paginator import Paginator, EmptyPage
28from django.db.models import Q
29from orm.models import Project, ProjectLayer
30from django.template import Context, Template
31from django.core.serializers.json import DjangoJSONEncoder
32from django.core.exceptions import FieldError
33from django.conf.urls import url, patterns
34
35import urls
36import types
37import json
38import collections
39import operator
40
41
42class ToasterTemplateView(TemplateView):
43 def get_context_data(self, **kwargs):
44 context = super(ToasterTemplateView, self).get_context_data(**kwargs)
45 if 'pid' in kwargs:
46 context['project'] = Project.objects.get(pk=kwargs['pid'])
47
48 context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project']))
49 return context
50
51
52class ToasterTable(View):
53 def __init__(self):
54 self.title = None
55 self.queryset = None
56 self.columns = []
57 self.filters = {}
58 self.total_count = 0
59 self.static_context_extra = {}
60 self.filter_actions = {}
61 self.empty_state = "Sorry - no data found"
62 self.default_orderby = ""
63
64 def get(self, request, *args, **kwargs):
65 self.setup_queryset(*args, **kwargs)
66
67 # Put the project id into the context for the static_data_template
68 if 'pid' in kwargs:
69 self.static_context_extra['pid'] = kwargs['pid']
70
71 cmd = kwargs['cmd']
72 if cmd and 'filterinfo' in cmd:
73 data = self.get_filter_info(request)
74 else:
75 # If no cmd is specified we give you the table data
76 data = self.get_data(request, **kwargs)
77
78 return HttpResponse(data, content_type="application/json")
79
80 def get_filter_info(self, request):
81 data = None
82
83 self.setup_filters()
84
85 search = request.GET.get("search", None)
86 if search:
87 self.apply_search(search)
88
89 name = request.GET.get("name", None)
90 if name is None:
91 data = json.dumps(self.filters,
92 indent=2,
93 cls=DjangoJSONEncoder)
94 else:
95 for actions in self.filters[name]['filter_actions']:
96 actions['count'] = self.filter_actions[actions['name']](count_only=True)
97
98 # Add the "All" items filter action
99 self.filters[name]['filter_actions'].insert(0, {
100 'name' : 'all',
101 'title' : 'All',
102 'count' : self.queryset.count(),
103 })
104
105 data = json.dumps(self.filters[name],
106 indent=2,
107 cls=DjangoJSONEncoder)
108
109 return data
110
111 def setup_columns(self, *args, **kwargs):
112 """ function to implement in the subclass which sets up the columns """
113 pass
114 def setup_filters(self, *args, **kwargs):
115 """ function to implement in the subclass which sets up the filters """
116 pass
117 def setup_queryset(self, *args, **kwargs):
118 """ function to implement in the subclass which sets up the queryset"""
119 pass
120
121 def add_filter(self, name, title, filter_actions):
122 """Add a filter to the table.
123
124 Args:
125 name (str): Unique identifier of the filter.
126 title (str): Title of the filter.
127 filter_actions: Actions for all the filters.
128 """
129 self.filters[name] = {
130 'title' : title,
131 'filter_actions' : filter_actions,
132 }
133
134 def make_filter_action(self, name, title, action_function):
135 """ Utility to make a filter_action """
136
137 action = {
138 'title' : title,
139 'name' : name,
140 }
141
142 self.filter_actions[name] = action_function
143
144 return action
145
146 def add_column(self, title="", help_text="",
147 orderable=False, hideable=True, hidden=False,
148 field_name="", filter_name=None, static_data_name=None,
149 static_data_template=None):
150 """Add a column to the table.
151
152 Args:
153 title (str): Title for the table header
154 help_text (str): Optional help text to describe the column
155 orderable (bool): Whether the column can be ordered.
156 We order on the field_name.
157 hideable (bool): Whether the user can hide the column
158 hidden (bool): Whether the column is default hidden
159 field_name (str or list): field(s) required for this column's data
160 static_data_name (str, optional): The column's main identifier
161 which will replace the field_name.
162 static_data_template(str, optional): The template to be rendered
163 as data
164 """
165
166 self.columns.append({'title' : title,
167 'help_text' : help_text,
168 'orderable' : orderable,
169 'hideable' : hideable,
170 'hidden' : hidden,
171 'field_name' : field_name,
172 'filter_name' : filter_name,
173 'static_data_name': static_data_name,
174 'static_data_template': static_data_template,
175 })
176
177 def render_static_data(self, template, row):
178 """Utility function to render the static data template"""
179
180 context = {
181 'extra' : self.static_context_extra,
182 'data' : row,
183 }
184
185 context = Context(context)
186 template = Template(template)
187
188 return template.render(context)
189
190 def apply_filter(self, filters):
191 self.setup_filters()
192
193 try:
194 filter_name, filter_action = filters.split(':')
195 except ValueError:
196 return
197
198 if "all" in filter_action:
199 return
200
201 try:
202 self.filter_actions[filter_action]()
203 except KeyError:
204 print "Filter and Filter action pair not found"
205
206 def apply_orderby(self, orderby):
207 # Note that django will execute this when we try to retrieve the data
208 self.queryset = self.queryset.order_by(orderby)
209
210 def apply_search(self, search_term):
211 """Creates a query based on the model's search_allowed_fields"""
212
213 if not hasattr(self.queryset.model, 'search_allowed_fields'):
214 print "Err Search fields aren't defined in the model"
215 return
216
217 search_queries = []
218 for st in search_term.split(" "):
219 q_map = [Q(**{field + '__icontains': st})
220 for field in self.queryset.model.search_allowed_fields]
221
222 search_queries.append(reduce(operator.or_, q_map))
223
224 search_queries = reduce(operator.and_, search_queries)
225 print "applied the search to the queryset"
226 self.queryset = self.queryset.filter(search_queries)
227
228 def get_data(self, request, **kwargs):
229 """Returns the data for the page requested with the specified
230 parameters applied"""
231
232 page_num = request.GET.get("page", 1)
233 limit = request.GET.get("limit", 10)
234 search = request.GET.get("search", None)
235 filters = request.GET.get("filter", None)
236 orderby = request.GET.get("orderby", None)
237
238 # Make a unique cache name
239 cache_name = self.__class__.__name__
240
241 for key, val in request.GET.iteritems():
242 cache_name = cache_name + str(key) + str(val)
243
244 data = cache.get(cache_name)
245
246 if data:
247 return data
248
249 self.setup_columns(**kwargs)
250
251 if search:
252 self.apply_search(search)
253 if filters:
254 self.apply_filter(filters)
255 if orderby:
256 self.apply_orderby(orderby)
257
258 paginator = Paginator(self.queryset, limit)
259
260 try:
261 page = paginator.page(page_num)
262 except EmptyPage:
263 page = paginator.page(1)
264
265 data = {
266 'total' : self.queryset.count(),
267 'default_orderby' : self.default_orderby,
268 'columns' : self.columns,
269 'rows' : [],
270 }
271
272 # Flatten all the fields we will need into one list
273 fields = []
274 for col in self.columns:
275 if type(col['field_name']) is list:
276 fields.extend(col['field_name'])
277 else:
278 fields.append(col['field_name'])
279
280 try:
281 for row in page.object_list:
282 #Use collection to maintain the order
283 required_data = collections.OrderedDict()
284
285 for col in self.columns:
286 field = col['field_name']
287 # Check if we need to process some static data
288 if "static_data_name" in col and col['static_data_name']:
289 required_data[col['static_data_name']] = self.render_static_data(col['static_data_template'], row)
290
291 # Overwrite the field_name with static_data_name
292 # so that this can be used as the html class name
293
294 col['field_name'] = col['static_data_name']
295 else:
296 model_data = row
297 # Traverse to any foriegn key in the object hierachy
298 for subfield in field.split("__"):
299 model_data = getattr(model_data, subfield)
300 # The field could be a function on the model so check
301 # If it is then call it
302 if isinstance(model_data, types.MethodType):
303 model_data = model_data()
304
305 required_data[field] = model_data
306
307 data['rows'].append(required_data)
308
309 except FieldError:
310 print "Error: Requested field does not exist"
311
312
313 data = json.dumps(data, indent=2, cls=DjangoJSONEncoder)
314 cache.set(cache_name, data, 10)
315
316 return data