410 lines
14 KiB
Python
410 lines
14 KiB
Python
#
|
|
# 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.views.decorators.cache import cache_control
|
|
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, Layer_Version
|
|
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 types
|
|
import json
|
|
import collections
|
|
import operator
|
|
import re
|
|
|
|
import logging
|
|
logger = logging.getLogger("toaster")
|
|
|
|
from toastergui.views import objtojson
|
|
|
|
class ToasterTable(TemplateView):
|
|
def __init__(self, *args, **kwargs):
|
|
super(ToasterTable, self).__init__()
|
|
if 'template_name' in kwargs:
|
|
self.template_name = kwargs['template_name']
|
|
self.title = "Table"
|
|
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 = ""
|
|
|
|
# add the "id" column, undisplayable, by default
|
|
self.add_column(title="Id",
|
|
displayable=False,
|
|
orderable=True,
|
|
field_name="id")
|
|
|
|
# prevent HTTP caching of table data
|
|
@cache_control(must_revalidate=True, max_age=0, no_store=True, no_cache=True)
|
|
def dispatch(self, *args, **kwargs):
|
|
return super(ToasterTable, self).dispatch(*args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(ToasterTable, self).get_context_data(**kwargs)
|
|
context['title'] = self.title
|
|
context['table_name'] = type(self).__name__.lower()
|
|
|
|
return context
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if request.GET.get('format', None) == 'json':
|
|
|
|
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 = request.GET.get('cmd', None)
|
|
if cmd and 'filterinfo' in cmd:
|
|
data = self.get_filter_info(request, **kwargs)
|
|
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")
|
|
|
|
return super(ToasterTable, self).get(request, *args, **kwargs)
|
|
|
|
def get_filter_info(self, request, **kwargs):
|
|
data = None
|
|
|
|
self.setup_filters(**kwargs)
|
|
|
|
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,
|
|
displayable=True, computation=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,
|
|
'displayable': displayable,
|
|
'computation': computation,
|
|
})
|
|
|
|
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, **kwargs):
|
|
self.setup_filters(**kwargs)
|
|
|
|
try:
|
|
filter_name, filter_action = filters.split(':')
|
|
except ValueError:
|
|
return
|
|
|
|
if "all" in filter_action:
|
|
return
|
|
|
|
try:
|
|
self.filter_actions[filter_action]()
|
|
except KeyError:
|
|
# pass it to the user - programming error here
|
|
raise
|
|
|
|
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'):
|
|
raise Exception("Search fields aren't defined in the model %s"
|
|
% self.queryset.model)
|
|
|
|
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)
|
|
|
|
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)
|
|
nocache = request.GET.get("nocache", None)
|
|
|
|
# Make a unique cache name
|
|
cache_name = self.__class__.__name__
|
|
|
|
for key, val in request.GET.iteritems():
|
|
if key == 'nocache':
|
|
continue
|
|
cache_name = cache_name + str(key) + str(val)
|
|
|
|
for key, val in kwargs.iteritems():
|
|
cache_name = cache_name + str(key) + str(val)
|
|
|
|
# No special chars allowed in the cache name apart from dash
|
|
cache_name = re.sub(r'[^A-Za-z0-9-]', "", cache_name)
|
|
|
|
if nocache:
|
|
cache.delete(cache_name)
|
|
|
|
data = cache.get(cache_name)
|
|
|
|
if data:
|
|
logger.debug("Got cache data for table '%s'" % self.title)
|
|
return data
|
|
|
|
self.setup_columns(**kwargs)
|
|
|
|
if search:
|
|
self.apply_search(search)
|
|
if filters:
|
|
self.apply_filter(filters, **kwargs)
|
|
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' : [],
|
|
'error' : "ok",
|
|
}
|
|
|
|
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']
|
|
if not field:
|
|
field = col['static_data_name']
|
|
if not field:
|
|
raise Exception("Must supply a field_name or static_data_name for column %s.%s" % (self.__class__.__name__,col))
|
|
# Check if we need to process some static data
|
|
if "static_data_name" in col and col['static_data_name']:
|
|
required_data["static:%s" % 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']
|
|
|
|
# compute the computation on the raw data if needed
|
|
model_data = row
|
|
if col['computation']:
|
|
model_data = col['computation'](row)
|
|
else:
|
|
# Traverse to any foriegn key in the object hierachy
|
|
for subfield in field.split("__"):
|
|
if hasattr(model_data, subfield):
|
|
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[col['field_name']] = model_data
|
|
|
|
data['rows'].append(required_data)
|
|
|
|
except FieldError:
|
|
# pass it to the user - programming-error here
|
|
raise
|
|
data = json.dumps(data, indent=2, default=objtojson)
|
|
cache.set(cache_name, data, 60*30)
|
|
|
|
return data
|
|
|
|
|
|
|
|
class ToasterTypeAhead(View):
|
|
""" A typeahead mechanism to support the front end typeahead widgets """
|
|
MAX_RESULTS = 6
|
|
|
|
class MissingFieldsException(Exception):
|
|
pass
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(ToasterTypeAhead, self).__init__()
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
def response(data):
|
|
return HttpResponse(json.dumps(data,
|
|
indent=2,
|
|
cls=DjangoJSONEncoder),
|
|
content_type="application/json")
|
|
|
|
error = "ok"
|
|
|
|
search_term = request.GET.get("search", None)
|
|
if search_term == None:
|
|
# We got no search value so return empty reponse
|
|
return response({'error' : error , 'results': []})
|
|
|
|
try:
|
|
prj = Project.objects.get(pk=kwargs['pid'])
|
|
except KeyError:
|
|
prj = None
|
|
|
|
results = self.apply_search(search_term, prj, request)[:ToasterTypeAhead.MAX_RESULTS]
|
|
|
|
if len(results) > 0:
|
|
try:
|
|
self.validate_fields(results[0])
|
|
except MissingFieldsException as e:
|
|
error = e
|
|
|
|
data = { 'results' : results,
|
|
'error' : error,
|
|
}
|
|
|
|
return response(data)
|
|
|
|
def validate_fields(self, result):
|
|
if 'name' in result == False or 'detail' in result == False:
|
|
raise MissingFieldsException("name and detail are required fields")
|
|
|
|
def apply_search(self, search_term, prj):
|
|
""" Override this function to implement search. Return an array of
|
|
dictionaries with a minium of a name and detail field"""
|
|
pass
|