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:
Michael Wood
2015-05-08 17:24:11 +01:00
committed by Richard Purdie
parent 2ac26e4294
commit 7f8c44771c
4 changed files with 943 additions and 0 deletions

View 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');
});
}

View File

@@ -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">&times;</button>
<span id="alert-msg"></span>
</div>
</div>
{% url table_name project.id as xhr_table_url %}
{% include "toastertable.html" %}
{% endblock %}

View 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>

View 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