generic-poky/bitbake/lib/toaster/toastergui/widgets.py

448 lines
16 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
try:
from urllib import unquote_plus
except ImportError:
from urllib.parse import unquote_plus
import logging
logger = logging.getLogger("toaster")
from toastergui.tablefilter import TableFilterMap
class NoFieldOrDataNme(Exception):
pass
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 = []
# map from field names to Filter instances
self.filter_map = TableFilterMap()
self.total_count = 0
self.static_context_extra = {}
self.empty_state = "Sorry - no data found"
self.default_orderby = ""
# 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()
context['empty_state'] = self.empty_state
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):
self.setup_filters(**kwargs)
search = request.GET.get("search", None)
if search:
self.apply_search(search)
name = request.GET.get("name", None)
table_filter = self.filter_map.get_filter(name)
return json.dumps(table_filter.to_json(self.queryset),
indent=2,
cls=DjangoJSONEncoder)
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, table_filter):
"""Add a filter to the table.
Args:
table_filter: Filter instance
"""
self.filter_map.add_filter(table_filter.name, table_filter)
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 set_column_hidden(self, title, hidden):
"""
Set the hidden state of the column to the value of hidden
"""
for col in self.columns:
if col['title'] == title:
col['hidden'] = hidden
break
def set_column_hideable(self, title, hideable):
"""
Set the hideable state of the column to the value of hideable
"""
for col in self.columns:
if col['title'] == title:
col['hideable'] = hideable
break
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, filter_value, **kwargs):
"""
Apply a filter submitted in the querystring to the ToasterTable
filters: (str) in the format:
'<filter name>:<action name>'
filter_value: (str) parameters to pass to the named filter
<filter name> and <action name> are used to look up the correct filter
in the ToasterTable's filter map; the <action params> are set on
TableFilterAction* before its filter is applied and may modify the
queryset returned by the filter
"""
self.setup_filters(**kwargs)
try:
filter_name, action_name = filters.split(':')
action_params = urllib.unquote_plus(filter_value)
except ValueError:
return
if "all" in action_name:
return
try:
table_filter = self.filter_map.get_filter(filter_name)
action = table_filter.get_action(action_name)
action.set_filter_params(action_params)
self.queryset = action.filter(self.queryset)
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 = None
for st in search_term.split(" "):
queries = None
for field in self.queryset.model.search_allowed_fields:
query = Q(**{field + '__icontains': st})
if queries:
queries |= query
else:
queries = query
if search_queries:
search_queries &= queries
else:
search_queries = 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
filters: filter and action name, e.g. "outcome:build_succeeded"
filter_value: value to pass to the named filter+action, e.g. "on"
(for a toggle filter) or "2015-12-11,2015-12-12"
(for a date range filter)
"""
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)
filter_value = request.GET.get("filter_value", "on")
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.items():
if key == 'nocache':
continue
cache_name = cache_name + str(key) + str(val)
for key, val in kwargs.items():
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, filter_value, **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 model_obj 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 NoFieldOrDataNme("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']:
# 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']
# Render the template given
required_data[col['static_data_name']] = \
self.render_static_data(
col['static_data_template'], model_obj)
else:
# Traverse to any foriegn key in the field
# e.g. recipe__layer_version__name
model_data = None
if "__" in field:
for subfield in field.split("__"):
if not model_data:
# The first iteration is always going to
# be on the actual model object instance.
# Subsequent ones are on the result of
# that. e.g. forieng key objects
model_data = getattr(model_obj,
subfield)
else:
model_data = getattr(model_data,
subfield)
else:
model_data = getattr(model_obj,
col['field_name'])
# We might have a model function as the field so
# call it to return the data needed
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, cls=DjangoJSONEncoder)
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