blob: eb2914d87c28a799aed85e1585e1fdf72091ec73 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
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, Layer_Version
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 types
36import json
37import collections
38import operator
39import re
40
41from toastergui.views import objtojson
42
43class ToasterTable(TemplateView):
44 def __init__(self, *args, **kwargs):
45 super(ToasterTable, self).__init__()
46 if 'template_name' in kwargs:
47 self.template_name = kwargs['template_name']
48 self.title = None
49 self.queryset = None
50 self.columns = []
51 self.filters = {}
52 self.total_count = 0
53 self.static_context_extra = {}
54 self.filter_actions = {}
55 self.empty_state = "Sorry - no data found"
56 self.default_orderby = ""
57
58 # add the "id" column, undisplayable, by default
59 self.add_column(title="Id",
60 displayable=False,
61 orderable=True,
62 field_name="id")
63
64
65 def get(self, request, *args, **kwargs):
66 if request.GET.get('format', None) == 'json':
67
68 self.setup_queryset(*args, **kwargs)
69 # Put the project id into the context for the static_data_template
70 if 'pid' in kwargs:
71 self.static_context_extra['pid'] = kwargs['pid']
72
73 cmd = request.GET.get('cmd', None)
74 if cmd and 'filterinfo' in cmd:
75 data = self.get_filter_info(request, **kwargs)
76 else:
77 # If no cmd is specified we give you the table data
78 data = self.get_data(request, **kwargs)
79
80 return HttpResponse(data, content_type="application/json")
81
82 return super(ToasterTable, self).get(request, *args, **kwargs)
83
84 def get_filter_info(self, request, **kwargs):
85 data = None
86
87 self.setup_filters(**kwargs)
88
89 search = request.GET.get("search", None)
90 if search:
91 self.apply_search(search)
92
93 name = request.GET.get("name", None)
94 if name is None:
95 data = json.dumps(self.filters,
96 indent=2,
97 cls=DjangoJSONEncoder)
98 else:
99 for actions in self.filters[name]['filter_actions']:
100 actions['count'] = self.filter_actions[actions['name']](count_only=True)
101
102 # Add the "All" items filter action
103 self.filters[name]['filter_actions'].insert(0, {
104 'name' : 'all',
105 'title' : 'All',
106 'count' : self.queryset.count(),
107 })
108
109 data = json.dumps(self.filters[name],
110 indent=2,
111 cls=DjangoJSONEncoder)
112
113 return data
114
115 def setup_columns(self, *args, **kwargs):
116 """ function to implement in the subclass which sets up the columns """
117 pass
118 def setup_filters(self, *args, **kwargs):
119 """ function to implement in the subclass which sets up the filters """
120 pass
121 def setup_queryset(self, *args, **kwargs):
122 """ function to implement in the subclass which sets up the queryset"""
123 pass
124
125 def add_filter(self, name, title, filter_actions):
126 """Add a filter to the table.
127
128 Args:
129 name (str): Unique identifier of the filter.
130 title (str): Title of the filter.
131 filter_actions: Actions for all the filters.
132 """
133 self.filters[name] = {
134 'title' : title,
135 'filter_actions' : filter_actions,
136 }
137
138 def make_filter_action(self, name, title, action_function):
139 """ Utility to make a filter_action """
140
141 action = {
142 'title' : title,
143 'name' : name,
144 }
145
146 self.filter_actions[name] = action_function
147
148 return action
149
150 def add_column(self, title="", help_text="",
151 orderable=False, hideable=True, hidden=False,
152 field_name="", filter_name=None, static_data_name=None,
153 displayable=True, computation=None,
154 static_data_template=None):
155 """Add a column to the table.
156
157 Args:
158 title (str): Title for the table header
159 help_text (str): Optional help text to describe the column
160 orderable (bool): Whether the column can be ordered.
161 We order on the field_name.
162 hideable (bool): Whether the user can hide the column
163 hidden (bool): Whether the column is default hidden
164 field_name (str or list): field(s) required for this column's data
165 static_data_name (str, optional): The column's main identifier
166 which will replace the field_name.
167 static_data_template(str, optional): The template to be rendered
168 as data
169 """
170
171 self.columns.append({'title' : title,
172 'help_text' : help_text,
173 'orderable' : orderable,
174 'hideable' : hideable,
175 'hidden' : hidden,
176 'field_name' : field_name,
177 'filter_name' : filter_name,
178 'static_data_name': static_data_name,
179 'static_data_template': static_data_template,
180 'displayable': displayable,
181 'computation': computation,
182 })
183
184 def render_static_data(self, template, row):
185 """Utility function to render the static data template"""
186
187 context = {
188 'extra' : self.static_context_extra,
189 'data' : row,
190 }
191
192 context = Context(context)
193 template = Template(template)
194
195 return template.render(context)
196
197 def apply_filter(self, filters, **kwargs):
198 self.setup_filters(**kwargs)
199
200 try:
201 filter_name, filter_action = filters.split(':')
202 except ValueError:
203 return
204
205 if "all" in filter_action:
206 return
207
208 try:
209 self.filter_actions[filter_action]()
210 except KeyError:
211 # pass it to the user - programming error here
212 raise
213
214 def apply_orderby(self, orderby):
215 # Note that django will execute this when we try to retrieve the data
216 self.queryset = self.queryset.order_by(orderby)
217
218 def apply_search(self, search_term):
219 """Creates a query based on the model's search_allowed_fields"""
220
221 if not hasattr(self.queryset.model, 'search_allowed_fields'):
222 raise Exception("Err Search fields aren't defined in the model")
223
224 search_queries = []
225 for st in search_term.split(" "):
226 q_map = [Q(**{field + '__icontains': st})
227 for field in self.queryset.model.search_allowed_fields]
228
229 search_queries.append(reduce(operator.or_, q_map))
230
231 search_queries = reduce(operator.and_, search_queries)
232
233 self.queryset = self.queryset.filter(search_queries)
234
235
236 def get_data(self, request, **kwargs):
237 """Returns the data for the page requested with the specified
238 parameters applied"""
239
240 page_num = request.GET.get("page", 1)
241 limit = request.GET.get("limit", 10)
242 search = request.GET.get("search", None)
243 filters = request.GET.get("filter", None)
244 orderby = request.GET.get("orderby", None)
245
246 # Make a unique cache name
247 cache_name = self.__class__.__name__
248
249 for key, val in request.GET.iteritems():
250 cache_name = cache_name + str(key) + str(val)
251
252 for key, val in kwargs.iteritems():
253 cache_name = cache_name + str(key) + str(val)
254
255 # No special chars allowed in the cache name apart from dash
256 cache_name = re.sub(r'[^A-Za-z0-9-]', "", cache_name)
257 data = cache.get(cache_name)
258
259 if data:
260 return data
261
262 self.setup_columns(**kwargs)
263
264 if search:
265 self.apply_search(search)
266 if filters:
267 self.apply_filter(filters, **kwargs)
268 if orderby:
269 self.apply_orderby(orderby)
270
271 paginator = Paginator(self.queryset, limit)
272
273 try:
274 page = paginator.page(page_num)
275 except EmptyPage:
276 page = paginator.page(1)
277
278 data = {
279 'total' : self.queryset.count(),
280 'default_orderby' : self.default_orderby,
281 'columns' : self.columns,
282 'rows' : [],
283 'error' : "ok",
284 }
285
286 try:
287 for row in page.object_list:
288 #Use collection to maintain the order
289 required_data = collections.OrderedDict()
290
291 for col in self.columns:
292 field = col['field_name']
293 if not field:
294 field = col['static_data_name']
295 if not field:
296 raise Exception("Must supply a field_name or static_data_name for column %s.%s" % (self.__class__.__name__,col))
297 # Check if we need to process some static data
298 if "static_data_name" in col and col['static_data_name']:
299 required_data["static:%s" % col['static_data_name']] = self.render_static_data(col['static_data_template'], row)
300
301 # Overwrite the field_name with static_data_name
302 # so that this can be used as the html class name
303
304 col['field_name'] = col['static_data_name']
305
306 # compute the computation on the raw data if needed
307 model_data = row
308 if col['computation']:
309 model_data = col['computation'](row)
310 else:
311 # Traverse to any foriegn key in the object hierachy
312 for subfield in field.split("__"):
313 if hasattr(model_data, subfield):
314 model_data = getattr(model_data, subfield)
315 # The field could be a function on the model so check
316 # If it is then call it
317 if isinstance(model_data, types.MethodType):
318 model_data = model_data()
319
320 required_data[col['field_name']] = model_data
321
322 data['rows'].append(required_data)
323
324 except FieldError:
325 # pass it to the user - programming-error here
326 raise
327 data = json.dumps(data, indent=2, default=objtojson)
328 cache.set(cache_name, data, 60*30)
329
330 return data
331
332
333class ToasterTemplateView(TemplateView):
334 # renders a instance in a template, or returns the context as json
335 # the class-equivalent of the _template_renderer decorator for views
336
337 def __init__(self, *args, **kwargs):
338 super(ToasterTemplateView, self).__init__(*args, **kwargs)
339 self.context_entries = []
340
341 def get(self, *args, **kwargs):
342 if self.request.GET.get('format', None) == 'json':
343 from django.core.urlresolvers import reverse
344 from django.shortcuts import HttpResponse
345 from views import objtojson
346 from toastergui.templatetags.projecttags import json as jsonfilter
347
348 context = self.get_context_data(**kwargs)
349
350 for x in context.keys():
351 if x not in self.context_entries:
352 del context[x]
353
354 context["error"] = "ok"
355
356 return HttpResponse(jsonfilter(context, default=objtojson ),
357 content_type = "application/json; charset=utf-8")
358
359 return super(ToasterTemplateView, self).get(*args, **kwargs)
360
361class ToasterTypeAhead(View):
362 """ A typeahead mechanism to support the front end typeahead widgets """
363 MAX_RESULTS = 6
364
365 class MissingFieldsException(Exception):
366 pass
367
368 def __init__(self, *args, **kwargs):
369 super(ToasterTypeAhead, self).__init__()
370
371 def get(self, request, *args, **kwargs):
372 def response(data):
373 return HttpResponse(json.dumps(data,
374 indent=2,
375 cls=DjangoJSONEncoder),
376 content_type="application/json")
377
378 error = "ok"
379
380 search_term = request.GET.get("search", None)
381 if search_term == None:
382 # We got no search value so return empty reponse
383 return response({'error' : error , 'results': []})
384
385 try:
386 prj = Project.objects.get(pk=kwargs['pid'])
387 except KeyError:
388 prj = None
389
390 results = self.apply_search(search_term, prj, request)[:ToasterTypeAhead.MAX_RESULTS]
391
392 if len(results) > 0:
393 try:
394 self.validate_fields(results[0])
395 except MissingFieldsException as e:
396 error = e
397
398 data = { 'results' : results,
399 'error' : error,
400 }
401
402 return response(data)
403
404 def validate_fields(self, result):
405 if 'name' in result == False or 'detail' in result == False:
406 raise MissingFieldsException("name and detail are required fields")
407
408 def apply_search(self, search_term, prj):
409 """ Override this function to implement search. Return an array of
410 dictionaries with a minium of a name and detail field"""
411 pass