mirror of
https://git.yoctoproject.org/poky
synced 2026-04-30 21:32:13 +02:00
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>
This commit is contained in:
committed by
Richard Purdie
parent
2ac26e4294
commit
7f8c44771c
485
bitbake/lib/toaster/toastergui/static/js/table.js
Normal file
485
bitbake/lib/toaster/toastergui/static/js/table.js
Normal file
@@ -0,0 +1,485 @@
|
||||
'use strict';
|
||||
|
||||
function tableInit(ctx){
|
||||
|
||||
if (ctx.url.length === 0) {
|
||||
throw "No url supplied for retreiving data";
|
||||
}
|
||||
|
||||
var tableChromeDone = false;
|
||||
var tableTotal = 0;
|
||||
|
||||
var tableParams = {
|
||||
limit : 25,
|
||||
page : 1,
|
||||
orderby : null,
|
||||
filter : null,
|
||||
search : null,
|
||||
};
|
||||
|
||||
var defaultHiddenCols = [];
|
||||
|
||||
var table = $("#" + ctx.tableName);
|
||||
|
||||
/* if we're loading clean from a url use it's parameters as the default */
|
||||
var urlParams = libtoaster.parseUrlParams();
|
||||
|
||||
/* Merge the tableParams and urlParams object properties */
|
||||
tableParams = $.extend(tableParams, urlParams);
|
||||
|
||||
/* Now fix the types that .extend changed for us */
|
||||
tableParams.limit = Number(tableParams.limit);
|
||||
tableParams.page = Number(tableParams.page);
|
||||
|
||||
loadData(tableParams);
|
||||
|
||||
window.onpopstate = function(event){
|
||||
if (event.state){
|
||||
tableParams = event.state.tableParams;
|
||||
/* We skip loadData and just update the table */
|
||||
updateTable(event.state.tableData);
|
||||
}
|
||||
};
|
||||
|
||||
function loadData(tableParams){
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: ctx.url,
|
||||
data: tableParams,
|
||||
headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
|
||||
success: function(tableData) {
|
||||
updateTable(tableData);
|
||||
window.history.pushState({
|
||||
tableData: tableData,
|
||||
tableParams: tableParams
|
||||
}, null, libtoaster.dumpsUrlParams(tableParams));
|
||||
},
|
||||
|
||||
error: function (_data) {
|
||||
console.warn("Call failed");
|
||||
console.warn(_data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateTable(tableData) {
|
||||
var tableBody = table.children("tbody");
|
||||
var paginationBtns = $('#pagination-'+ctx.tableName);
|
||||
|
||||
/* To avoid page re-layout flicker when paging set fixed height */
|
||||
table.css("visibility", "hidden");
|
||||
table.css("padding-bottom", table.height());
|
||||
|
||||
/* Reset table components */
|
||||
tableBody.html("");
|
||||
paginationBtns.html("");
|
||||
|
||||
if (tableParams.search)
|
||||
$('.remove-search-btn-'+ctx.tableName).show();
|
||||
else
|
||||
$('.remove-search-btn-'+ctx.tableName).hide();
|
||||
|
||||
$('.table-count-' + ctx.tableName).text(tableData.total);
|
||||
tableTotal = tableData.total;
|
||||
|
||||
if (tableData.total === 0){
|
||||
$("#table-container-"+ctx.tableName).hide();
|
||||
$("#new-search-input-"+ctx.tableName).val(tableParams.search);
|
||||
$("#no-results-"+ctx.tableName).show();
|
||||
return;
|
||||
} else {
|
||||
$("#table-container-"+ctx.tableName).show();
|
||||
$("#no-results-"+ctx.tableName).hide();
|
||||
}
|
||||
|
||||
|
||||
setupTableChrome(tableData);
|
||||
|
||||
|
||||
/* Add table data rows */
|
||||
for (var i in tableData.rows){
|
||||
var row = $("<tr></tr>");
|
||||
for (var key_j in tableData.rows[i]){
|
||||
var td = $("<td></td>");
|
||||
td.prop("class", key_j);
|
||||
if (tableData.rows[i][key_j]){
|
||||
td.html(tableData.rows[i][key_j]);
|
||||
}
|
||||
row.append(td);
|
||||
}
|
||||
tableBody.append(row);
|
||||
|
||||
/* If we have layerbtns then initialise them */
|
||||
layerBtnsInit(ctx);
|
||||
|
||||
/* If we have popovers initialise them now */
|
||||
$('td > a.btn').popover({
|
||||
html:true,
|
||||
placement:'left',
|
||||
container:'body',
|
||||
trigger:'manual'
|
||||
}).click(function(e){
|
||||
$('td > a.btn').not(this).popover('hide');
|
||||
/* ideally we would use 'toggle' here
|
||||
* but it seems buggy in our Bootstrap version
|
||||
*/
|
||||
$(this).popover('show');
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
/* enable help information tooltip */
|
||||
$(".get-help").tooltip({container:'body', html:true, delay:{show:300}});
|
||||
}
|
||||
|
||||
/* Setup the pagination controls */
|
||||
|
||||
var start = tableParams.page - 2;
|
||||
var end = tableParams.page + 2;
|
||||
var numPages = Math.ceil(tableData.total/tableParams.limit);
|
||||
|
||||
if (tableParams.page < 3)
|
||||
end = 5;
|
||||
|
||||
for (var page_i=1; page_i <= numPages; page_i++){
|
||||
if (page_i >= start && page_i <= end){
|
||||
var btn = $('<li><a href="#" class="page">'+page_i+'</a></li>');
|
||||
|
||||
if (page_i === tableParams.page){
|
||||
btn.addClass("active");
|
||||
}
|
||||
|
||||
/* Add the click handler */
|
||||
btn.click(pageButtonClicked);
|
||||
paginationBtns.append(btn);
|
||||
}
|
||||
}
|
||||
table.css("padding-bottom", 0);
|
||||
loadColumnsPreference();
|
||||
|
||||
$("table").css("visibility", "visible");
|
||||
}
|
||||
|
||||
function setupTableChrome(tableData){
|
||||
if (tableChromeDone === true)
|
||||
return;
|
||||
|
||||
var tableHeadRow = table.find("thead tr");
|
||||
var editColMenu = $("#table-chrome-"+ctx.tableName).find(".editcol");
|
||||
|
||||
tableHeadRow.html("");
|
||||
editColMenu.html("");
|
||||
|
||||
if (!tableParams.orderby && tableData.default_orderby){
|
||||
tableParams.orderby = tableData.default_orderby;
|
||||
}
|
||||
|
||||
/* Add table header and column toggle menu */
|
||||
for (var i in tableData.columns){
|
||||
var col = tableData.columns[i];
|
||||
var header = $("<th></th>");
|
||||
header.prop("class", col.field_name);
|
||||
|
||||
/* Setup the help text */
|
||||
if (col.help_text.length > 0) {
|
||||
var help_text = $('<i class="icon-question-sign get-help"> </i>');
|
||||
help_text.tooltip({title: col.help_text});
|
||||
header.append(help_text);
|
||||
}
|
||||
|
||||
/* Setup the orderable title */
|
||||
if (col.orderable) {
|
||||
var title = $('<a href=\"#\" ></a>');
|
||||
|
||||
title.data('field-name', col.field_name);
|
||||
title.text(col.title);
|
||||
title.click(sortColumnClicked);
|
||||
|
||||
header.append(title);
|
||||
|
||||
header.append(' <i class="icon-caret-down" style="display:none"></i>');
|
||||
header.append(' <i class="icon-caret-up" style="display:none"></i>');
|
||||
|
||||
/* If we're currently ordered setup the visual indicator */
|
||||
if (col.field_name === tableParams.orderby ||
|
||||
'-' + col.field_name === tableParams.orderby){
|
||||
header.children("a").addClass("sorted");
|
||||
|
||||
if (tableParams.orderby.indexOf("-") === -1){
|
||||
header.find('.icon-caret-down').show();
|
||||
} else {
|
||||
header.find('.icon-caret-up').show();
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
/* Not orderable */
|
||||
header.addClass("muted");
|
||||
header.css("font-weight", "normal");
|
||||
header.append(col.title+' ');
|
||||
}
|
||||
|
||||
/* Setup the filter button */
|
||||
if (col.filter_name){
|
||||
var filterBtn = $('<a href="#" role="button" class="pull-right btn btn-mini" data-toggle="modal"><i class="icon-filter filtered"></i></a>');
|
||||
|
||||
filterBtn.data('filter-name', col.filter_name);
|
||||
filterBtn.click(filterOpenClicked);
|
||||
|
||||
/* If we're currently being filtered setup the visial indicator */
|
||||
if (tableParams.filter &&
|
||||
tableParams.filter.match('^'+col.filter_name)) {
|
||||
|
||||
filterBtn.addClass("btn-primary");
|
||||
|
||||
filterBtn.tooltip({
|
||||
html: true,
|
||||
title: '<button class="btn btn-small btn-primary" onClick=\'$("#clear-filter-btn").click();\'>Clear filter</button>',
|
||||
placement: 'bottom',
|
||||
delay: {
|
||||
hide: 1500,
|
||||
show: 400,
|
||||
},
|
||||
});
|
||||
}
|
||||
header.append(filterBtn);
|
||||
}
|
||||
|
||||
/* Done making the header now add it */
|
||||
tableHeadRow.append(header);
|
||||
|
||||
/* Now setup the checkbox state and click handler */
|
||||
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>');
|
||||
|
||||
var togglerInput = toggler.find("input");
|
||||
|
||||
togglerInput.attr("checked","checked");
|
||||
|
||||
/* If we can hide the column enable the checkbox action */
|
||||
if (col.hideable){
|
||||
togglerInput.click(colToggleClicked);
|
||||
} else {
|
||||
toggler.find("label").addClass("muted");
|
||||
togglerInput.attr("disabled", "disabled");
|
||||
}
|
||||
|
||||
if (col.hidden) {
|
||||
defaultHiddenCols.push(col.field_name);
|
||||
}
|
||||
|
||||
editColMenu.append(toggler);
|
||||
} /* End for each column */
|
||||
|
||||
tableChromeDone = true;
|
||||
}
|
||||
|
||||
/* Display or hide table columns based on the cookie preference or defaults */
|
||||
function loadColumnsPreference(){
|
||||
var cookie_data = $.cookie("cols");
|
||||
|
||||
if (cookie_data) {
|
||||
var cols_hidden = JSON.parse($.cookie("cols"));
|
||||
|
||||
/* For each of the columns check if we should hide them
|
||||
* also update the checked status in the Edit columns menu
|
||||
*/
|
||||
$("#"+ctx.tableName+" th").each(function(){
|
||||
for (var i in cols_hidden){
|
||||
if ($(this).hasClass(cols_hidden[i])){
|
||||
$("."+cols_hidden[i]).hide();
|
||||
$("#checkbox-"+cols_hidden[i]).removeAttr("checked");
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
/* Disable these columns by default when we have no columns
|
||||
* user setting.
|
||||
*/
|
||||
for (var i in defaultHiddenCols) {
|
||||
$("."+defaultHiddenCols[i]).hide();
|
||||
$("#checkbox-"+defaultHiddenCols[i]).removeAttr("checked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sortColumnClicked(){
|
||||
|
||||
/* We only have one sort at a time so remove any existing sort indicators */
|
||||
$("#"+ctx.tableName+" th .icon-caret-down").hide();
|
||||
$("#"+ctx.tableName+" th .icon-caret-up").hide();
|
||||
$("#"+ctx.tableName+" th a").removeClass("sorted");
|
||||
|
||||
var fieldName = $(this).data('field-name');
|
||||
|
||||
/* if we're already sorted sort the other way */
|
||||
if (tableParams.orderby === fieldName &&
|
||||
tableParams.orderby.indexOf('-') === -1) {
|
||||
tableParams.orderby = '-' + $(this).data('field-name');
|
||||
$(this).parent().children('.icon-caret-up').show();
|
||||
} else {
|
||||
tableParams.orderby = $(this).data('field-name');
|
||||
$(this).parent().children('.icon-caret-down').show();
|
||||
}
|
||||
|
||||
$(this).addClass("sorted");
|
||||
|
||||
loadData(tableParams);
|
||||
}
|
||||
|
||||
function pageButtonClicked(e) {
|
||||
tableParams.page = Number($(this).text());
|
||||
loadData(tableParams);
|
||||
/* Stop page jumps when clicking on # links */
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
/* Toggle a table column */
|
||||
function colToggleClicked (){
|
||||
var col = $(this).val();
|
||||
var disabled_cols = [];
|
||||
|
||||
if ($(this).prop("checked")) {
|
||||
$("."+col).show();
|
||||
} else {
|
||||
$("."+col).hide();
|
||||
/* If we're ordered by the column we're hiding remove the order by */
|
||||
if (col === tableParams.orderby ||
|
||||
'-' + col === tableParams.orderby){
|
||||
tableParams.orderby = null;
|
||||
loadData(tableParams);
|
||||
}
|
||||
}
|
||||
|
||||
/* Update the cookie with the unchecked columns */
|
||||
$(".col-toggle").not(":checked").map(function(){
|
||||
disabled_cols.push($(this).val());
|
||||
});
|
||||
|
||||
$.cookie("cols", JSON.stringify(disabled_cols));
|
||||
}
|
||||
|
||||
function filterOpenClicked(){
|
||||
var filterName = $(this).data('filter-name');
|
||||
|
||||
/* We need to pass in the curren search so that the filter counts take
|
||||
* into account the current search filter
|
||||
*/
|
||||
var params = {
|
||||
'name' : filterName,
|
||||
'search': tableParams.search
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: ctx.url + 'filterinfo',
|
||||
data: params,
|
||||
headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
|
||||
success: function (filterData) {
|
||||
var filterActionRadios = $('#filter-actions');
|
||||
|
||||
$('#filter-modal-title').text(filterData.title);
|
||||
|
||||
filterActionRadios.text("");
|
||||
|
||||
for (var i in filterData.filter_actions){
|
||||
var filterAction = filterData.filter_actions[i];
|
||||
|
||||
var action = $('<label class="radio"><input type="radio" name="filter" value=""><span class="filter-title"></span></label>');
|
||||
var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
|
||||
|
||||
var radioInput = action.children("input");
|
||||
|
||||
action.children(".filter-title").text(actionTitle);
|
||||
|
||||
radioInput.val(filterName + ':' + filterAction.name);
|
||||
|
||||
/* Setup the current selected filter, default to 'all' if
|
||||
* no current filter selected.
|
||||
*/
|
||||
if ((tableParams.filter &&
|
||||
tableParams.filter === radioInput.val()) ||
|
||||
filterAction.name == 'all') {
|
||||
radioInput.attr("checked", "checked");
|
||||
}
|
||||
|
||||
filterActionRadios.append(action);
|
||||
}
|
||||
|
||||
$('#filter-modal').modal('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$(".get-help").tooltip({container:'body', html:true, delay:{show:300}});
|
||||
|
||||
/* Keep the Edit columns menu open after click by eating the event */
|
||||
$('.dropdown-menu').click(function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
$(".pagesize").val(tableParams.limit);
|
||||
|
||||
/* page size selector */
|
||||
$(".pagesize").change(function(){
|
||||
tableParams.limit = Number(this.value);
|
||||
if ((tableParams.page * tableParams.limit) > tableTotal)
|
||||
tableParams.page = 1;
|
||||
|
||||
loadData(tableParams);
|
||||
/* sync the other selectors on the page */
|
||||
$(".pagesize").val(this.value);
|
||||
});
|
||||
|
||||
$("#search-submit-"+ctx.tableName).click(function(e){
|
||||
var searchTerm = $("#search-input-"+ctx.tableName).val();
|
||||
|
||||
tableParams.page = 1;
|
||||
tableParams.search = searchTerm;
|
||||
tableParams.filter = null;
|
||||
|
||||
loadData(tableParams);
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$('.remove-search-btn-'+ctx.tableName).click(function(e){
|
||||
e.preventDefault();
|
||||
|
||||
tableParams.page = 1;
|
||||
tableParams.search = null;
|
||||
loadData(tableParams);
|
||||
|
||||
$("#search-input-"+ctx.tableName).val("");
|
||||
$(this).hide();
|
||||
});
|
||||
|
||||
$("#search-input-"+ctx.tableName).keyup(function(e){
|
||||
if (e.which === 13)
|
||||
$('#search-submit-'+ctx.tableName).click();
|
||||
});
|
||||
|
||||
/* Stop page jumps when clicking on # links */
|
||||
$('a[href="#"]').click(function(e){
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$("#clear-filter-btn").click(function(){
|
||||
tableParams.filter = null;
|
||||
loadData(tableParams);
|
||||
});
|
||||
|
||||
$("#filter-modal-form").submit(function(e){
|
||||
e.preventDefault();
|
||||
|
||||
tableParams.filter = $(this).find("input[type='radio']:checked").val();
|
||||
|
||||
/* All === remove filter */
|
||||
if (tableParams.filter.match(":all$"))
|
||||
tableParams.filter = null;
|
||||
|
||||
loadData(tableParams);
|
||||
|
||||
|
||||
$('#filter-modal').modal('hide');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "baseprojectpage.html" %}
|
||||
{% load projecttags %}
|
||||
{% load humanize %}
|
||||
{% load static %}
|
||||
|
||||
{% block localbreadcrumb %}
|
||||
|
||||
|
||||
<li>{{title}}</li>{% endblock %}
|
||||
|
||||
{% block projectinfomain %}
|
||||
<div class="page-header">
|
||||
<h1>{{title}} (<span class="table-count-{{table_name}}"></span>)
|
||||
<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>
|
||||
</h1>
|
||||
</div>
|
||||
<div id="zone1alerts" style="display:none">
|
||||
<div class="alert alert-info lead">
|
||||
<button type="button" class="close" id="hide-alert">×</button>
|
||||
<span id="alert-msg"></span>
|
||||
</div>
|
||||
</div>
|
||||
{% url table_name project.id as xhr_table_url %}
|
||||
{% include "toastertable.html" %}
|
||||
{% endblock %}
|
||||
117
bitbake/lib/toaster/toastergui/templates/toastertable.html
Normal file
117
bitbake/lib/toaster/toastergui/templates/toastertable.html
Normal file
@@ -0,0 +1,117 @@
|
||||
|
||||
{% load static %}
|
||||
{% load projecttags %}
|
||||
|
||||
<script src="{% static 'js/table.js' %}"></script>
|
||||
<script src="{% static 'js/layerBtn.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
(function(){
|
||||
|
||||
var ctx = {
|
||||
tableName : "{{table_name}}",
|
||||
url : "{{ xhr_table_url }}",
|
||||
title : "{{title}}",
|
||||
projectLayers : {{projectlayers|json}},
|
||||
};
|
||||
|
||||
try {
|
||||
tableInit(ctx);
|
||||
} catch (e) {
|
||||
document.write("Problem loading table widget: " + e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- filter modal -->
|
||||
<div id="filter-modal" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="false">
|
||||
<form id="filter-modal-form" style="margin-bottom: 0px">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
|
||||
<h3 id="filter-modal-title"></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Show:</p>
|
||||
<span id="filter-actions"></span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" type="submit">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<button id="clear-filter-btn" style="display:none"></button>
|
||||
|
||||
<div class="row-fluid alert" id="no-results-{{table_name}}" style="display:none">
|
||||
<form class="no-results input-append">
|
||||
<input class="input-xxlarge" id="new-search-input-{{table_name}}" name="search" type="text" placeholder="Search {{title|lower}}" value="{{request.GET.search}}"/>
|
||||
<a href="#" class="add-on btn remove-search-btn-{{table_name}}" tabindex="-1">
|
||||
<i class="icon-remove"></i>
|
||||
</a>
|
||||
<button class="btn search-submit-{{table_name}}" >Search</button>
|
||||
<button class="btn btn-link remove-search-btn-{{table_name}}">Show {{title|lower}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="table-container-{{table_name}}">
|
||||
<!-- control header -->
|
||||
<div class="navbar" id="table-chrome-{{table_name}}">
|
||||
<div class="navbar-inner">
|
||||
<div class="navbar-search input-append pull-left">
|
||||
|
||||
<input class="input-xxlarge" id="search-input-{{table_name}}" name="search" type="text" placeholder="Search {{title|lower}}" value="{{request.GET.search}}"/>
|
||||
<a href="#" style="display:none" class="add-on btn remove-search-btn-{{table_name}}" tabindex="-1">
|
||||
<i class="icon-remove"></i>
|
||||
</a>
|
||||
<button class="btn" id="search-submit-{{table_name}}" >Search</button>
|
||||
</div>
|
||||
|
||||
<div class="pull-right">
|
||||
<div class="btn-group">
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">Edit columns
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu editcol">
|
||||
</ul>
|
||||
</div>
|
||||
<div style="display:inline">
|
||||
<span class="divider-vertical"></span>
|
||||
<span class="help-inline" style="padding-top:5px;">Show rows:</span>
|
||||
<select style="margin-top:5px;margin-bottom:0px;" class="pagesize">
|
||||
{% with "10 25 50 100 150" as list%}
|
||||
{% for i in list.split %}
|
||||
<option value="{{i}}">{{i}}</option>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The actual table -->
|
||||
<table class="table table-bordered table-hover tablesorter" id="{{table_name}}">
|
||||
<thead>
|
||||
<tr></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<div class="pagination pagination-centered">
|
||||
<ul id="pagination-{{table_name}}" class="pagination" style="display: block-inline">
|
||||
</ul>
|
||||
|
||||
<div class="pull-right">
|
||||
<span class="help-inline" style="padding-top:5px;">Show rows:</span>
|
||||
<select style="margin-top:5px;margin-bottom:0px;" class="pagesize">
|
||||
{% with "10 25 50 100 150" as list%}
|
||||
{% for i in list.split %}
|
||||
<option value="{{i}}">{{i}}</option>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
316
bitbake/lib/toaster/toastergui/widgets.py
Normal file
316
bitbake/lib/toaster/toastergui/widgets.py
Normal file
@@ -0,0 +1,316 @@
|
||||
#
|
||||
# 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.
|
||||
|
||||
from django.views.generic import View, TemplateView
|
||||
from django.shortcuts import HttpResponse
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.core import serializers
|
||||
from django.core.cache import cache
|
||||
from django.core.paginator import Paginator, EmptyPage
|
||||
from django.db.models import Q
|
||||
from orm.models import Project, ProjectLayer
|
||||
from django.template import Context, Template
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.core.exceptions import FieldError
|
||||
from django.conf.urls import url, patterns
|
||||
|
||||
import urls
|
||||
import types
|
||||
import json
|
||||
import collections
|
||||
import operator
|
||||
|
||||
|
||||
class ToasterTemplateView(TemplateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ToasterTemplateView, self).get_context_data(**kwargs)
|
||||
if 'pid' in kwargs:
|
||||
context['project'] = Project.objects.get(pk=kwargs['pid'])
|
||||
|
||||
context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project']))
|
||||
return context
|
||||
|
||||
|
||||
class ToasterTable(View):
|
||||
def __init__(self):
|
||||
self.title = None
|
||||
self.queryset = None
|
||||
self.columns = []
|
||||
self.filters = {}
|
||||
self.total_count = 0
|
||||
self.static_context_extra = {}
|
||||
self.filter_actions = {}
|
||||
self.empty_state = "Sorry - no data found"
|
||||
self.default_orderby = ""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.setup_queryset(*args, **kwargs)
|
||||
|
||||
# Put the project id into the context for the static_data_template
|
||||
if 'pid' in kwargs:
|
||||
self.static_context_extra['pid'] = kwargs['pid']
|
||||
|
||||
cmd = kwargs['cmd']
|
||||
if cmd and 'filterinfo' in cmd:
|
||||
data = self.get_filter_info(request)
|
||||
else:
|
||||
# If no cmd is specified we give you the table data
|
||||
data = self.get_data(request, **kwargs)
|
||||
|
||||
return HttpResponse(data, content_type="application/json")
|
||||
|
||||
def get_filter_info(self, request):
|
||||
data = None
|
||||
|
||||
self.setup_filters()
|
||||
|
||||
search = request.GET.get("search", None)
|
||||
if search:
|
||||
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']:
|
||||
actions['count'] = self.filter_actions[actions['name']](count_only=True)
|
||||
|
||||
# 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
|
||||
|
||||
def setup_columns(self, *args, **kwargs):
|
||||
""" function to implement in the subclass which sets up the columns """
|
||||
pass
|
||||
def setup_filters(self, *args, **kwargs):
|
||||
""" function to implement in the subclass which sets up the filters """
|
||||
pass
|
||||
def setup_queryset(self, *args, **kwargs):
|
||||
""" function to implement in the subclass which sets up the queryset"""
|
||||
pass
|
||||
|
||||
def add_filter(self, name, title, filter_actions):
|
||||
"""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.
|
||||
"""
|
||||
self.filters[name] = {
|
||||
'title' : title,
|
||||
'filter_actions' : filter_actions,
|
||||
}
|
||||
|
||||
def make_filter_action(self, name, title, action_function):
|
||||
""" Utility to make a filter_action """
|
||||
|
||||
action = {
|
||||
'title' : title,
|
||||
'name' : name,
|
||||
}
|
||||
|
||||
self.filter_actions[name] = action_function
|
||||
|
||||
return action
|
||||
|
||||
def add_column(self, title="", help_text="",
|
||||
orderable=False, hideable=True, hidden=False,
|
||||
field_name="", filter_name=None, static_data_name=None,
|
||||
static_data_template=None):
|
||||
"""Add a column to the table.
|
||||
|
||||
Args:
|
||||
title (str): Title for the table header
|
||||
help_text (str): Optional help text to describe the column
|
||||
orderable (bool): Whether the column can be ordered.
|
||||
We order on the field_name.
|
||||
hideable (bool): Whether the user can hide the column
|
||||
hidden (bool): Whether the column is default hidden
|
||||
field_name (str or list): field(s) required for this column's data
|
||||
static_data_name (str, optional): The column's main identifier
|
||||
which will replace the field_name.
|
||||
static_data_template(str, optional): The template to be rendered
|
||||
as data
|
||||
"""
|
||||
|
||||
self.columns.append({'title' : title,
|
||||
'help_text' : help_text,
|
||||
'orderable' : orderable,
|
||||
'hideable' : hideable,
|
||||
'hidden' : hidden,
|
||||
'field_name' : field_name,
|
||||
'filter_name' : filter_name,
|
||||
'static_data_name': static_data_name,
|
||||
'static_data_template': static_data_template,
|
||||
})
|
||||
|
||||
def render_static_data(self, template, row):
|
||||
"""Utility function to render the static data template"""
|
||||
|
||||
context = {
|
||||
'extra' : self.static_context_extra,
|
||||
'data' : row,
|
||||
}
|
||||
|
||||
context = Context(context)
|
||||
template = Template(template)
|
||||
|
||||
return template.render(context)
|
||||
|
||||
def apply_filter(self, filters):
|
||||
self.setup_filters()
|
||||
|
||||
try:
|
||||
filter_name, filter_action = filters.split(':')
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if "all" in filter_action:
|
||||
return
|
||||
|
||||
try:
|
||||
self.filter_actions[filter_action]()
|
||||
except KeyError:
|
||||
print "Filter and Filter action pair not found"
|
||||
|
||||
def apply_orderby(self, orderby):
|
||||
# Note that django will execute this when we try to retrieve the data
|
||||
self.queryset = self.queryset.order_by(orderby)
|
||||
|
||||
def apply_search(self, search_term):
|
||||
"""Creates a query based on the model's search_allowed_fields"""
|
||||
|
||||
if not hasattr(self.queryset.model, 'search_allowed_fields'):
|
||||
print "Err Search fields aren't defined in the model"
|
||||
return
|
||||
|
||||
search_queries = []
|
||||
for st in search_term.split(" "):
|
||||
q_map = [Q(**{field + '__icontains': st})
|
||||
for field in self.queryset.model.search_allowed_fields]
|
||||
|
||||
search_queries.append(reduce(operator.or_, q_map))
|
||||
|
||||
search_queries = reduce(operator.and_, search_queries)
|
||||
print "applied the search to the queryset"
|
||||
self.queryset = self.queryset.filter(search_queries)
|
||||
|
||||
def get_data(self, request, **kwargs):
|
||||
"""Returns the data for the page requested with the specified
|
||||
parameters applied"""
|
||||
|
||||
page_num = request.GET.get("page", 1)
|
||||
limit = request.GET.get("limit", 10)
|
||||
search = request.GET.get("search", None)
|
||||
filters = request.GET.get("filter", None)
|
||||
orderby = request.GET.get("orderby", None)
|
||||
|
||||
# Make a unique cache name
|
||||
cache_name = self.__class__.__name__
|
||||
|
||||
for key, val in request.GET.iteritems():
|
||||
cache_name = cache_name + str(key) + str(val)
|
||||
|
||||
data = cache.get(cache_name)
|
||||
|
||||
if data:
|
||||
return data
|
||||
|
||||
self.setup_columns(**kwargs)
|
||||
|
||||
if search:
|
||||
self.apply_search(search)
|
||||
if filters:
|
||||
self.apply_filter(filters)
|
||||
if orderby:
|
||||
self.apply_orderby(orderby)
|
||||
|
||||
paginator = Paginator(self.queryset, limit)
|
||||
|
||||
try:
|
||||
page = paginator.page(page_num)
|
||||
except EmptyPage:
|
||||
page = paginator.page(1)
|
||||
|
||||
data = {
|
||||
'total' : self.queryset.count(),
|
||||
'default_orderby' : self.default_orderby,
|
||||
'columns' : self.columns,
|
||||
'rows' : [],
|
||||
}
|
||||
|
||||
# Flatten all the fields we will need into one list
|
||||
fields = []
|
||||
for col in self.columns:
|
||||
if type(col['field_name']) is list:
|
||||
fields.extend(col['field_name'])
|
||||
else:
|
||||
fields.append(col['field_name'])
|
||||
|
||||
try:
|
||||
for row in page.object_list:
|
||||
#Use collection to maintain the order
|
||||
required_data = collections.OrderedDict()
|
||||
|
||||
for col in self.columns:
|
||||
field = col['field_name']
|
||||
# Check if we need to process some static data
|
||||
if "static_data_name" in col and col['static_data_name']:
|
||||
required_data[col['static_data_name']] = self.render_static_data(col['static_data_template'], row)
|
||||
|
||||
# Overwrite the field_name with static_data_name
|
||||
# so that this can be used as the html class name
|
||||
|
||||
col['field_name'] = col['static_data_name']
|
||||
else:
|
||||
model_data = row
|
||||
# Traverse to any foriegn key in the object hierachy
|
||||
for subfield in field.split("__"):
|
||||
model_data = getattr(model_data, subfield)
|
||||
# The field could be a function on the model so check
|
||||
# If it is then call it
|
||||
if isinstance(model_data, types.MethodType):
|
||||
model_data = model_data()
|
||||
|
||||
required_data[field] = model_data
|
||||
|
||||
data['rows'].append(required_data)
|
||||
|
||||
except FieldError:
|
||||
print "Error: Requested field does not exist"
|
||||
|
||||
|
||||
data = json.dumps(data, indent=2, cls=DjangoJSONEncoder)
|
||||
cache.set(cache_name, data, 10)
|
||||
|
||||
return data
|
||||
Reference in New Issue
Block a user