summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/toaster/toastergui/widgets.py
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/lib/toaster/toastergui/widgets.py
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/lib/toaster/toastergui/widgets.py')
-rw-r--r--bitbake/lib/toaster/toastergui/widgets.py316
1 files changed, 316 insertions, 0 deletions
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