diff options
Diffstat (limited to 'bitbake/lib/toaster/toastergui/views.py')
-rw-r--r-- | bitbake/lib/toaster/toastergui/views.py | 412 |
1 files changed, 252 insertions, 160 deletions
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py index 7d4d710f83..09da9c2a2e 100644 --- a/bitbake/lib/toaster/toastergui/views.py +++ b/bitbake/lib/toaster/toastergui/views.py | |||
@@ -25,7 +25,10 @@ from orm.models import Task_Dependency, Recipe_Dependency, Package, Package_File | |||
25 | from orm.models import Target_Installed_Package | 25 | from orm.models import Target_Installed_Package |
26 | from django.views.decorators.cache import cache_control | 26 | from django.views.decorators.cache import cache_control |
27 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger | 27 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger |
28 | 28 | from django.http import HttpResponseBadRequest | |
29 | from django.utils import timezone | ||
30 | from datetime import timedelta | ||
31 | from django.utils import formats | ||
29 | 32 | ||
30 | def _build_page_range(paginator, index = 1): | 33 | def _build_page_range(paginator, index = 1): |
31 | try: | 34 | try: |
@@ -72,6 +75,109 @@ def _redirect_parameters(view, g, mandatory_parameters, *args, **kwargs): | |||
72 | 75 | ||
73 | return redirect(url + "?%s" % urllib.urlencode(params), *args, **kwargs) | 76 | return redirect(url + "?%s" % urllib.urlencode(params), *args, **kwargs) |
74 | 77 | ||
78 | FIELD_SEPARATOR = ":" | ||
79 | VALUE_SEPARATOR = ";" | ||
80 | DESCENDING = "-" | ||
81 | |||
82 | def __get_q_for_val(name, value): | ||
83 | if "OR" in value: | ||
84 | return reduce(operator.or_, map(lambda x: __get_q_for_val(name, x), [ x for x in value.split("OR") ])) | ||
85 | if "AND" in value: | ||
86 | return reduce(operator.and_, map(lambda x: __get_q_for_val(name, x), [ x for x in value.split("AND") ])) | ||
87 | if value.startswith("NOT"): | ||
88 | kwargs = { name : value.strip("NOT") } | ||
89 | return ~Q(**kwargs) | ||
90 | else: | ||
91 | kwargs = { name : value } | ||
92 | return Q(**kwargs) | ||
93 | |||
94 | def _get_filtering_query(filter_string): | ||
95 | |||
96 | search_terms = filter_string.split(FIELD_SEPARATOR) | ||
97 | keys = search_terms[0].split(VALUE_SEPARATOR) | ||
98 | values = search_terms[1].split(VALUE_SEPARATOR) | ||
99 | |||
100 | querydict = dict(zip(keys, values)) | ||
101 | return reduce(lambda x, y: x & y, map(lambda x: __get_q_for_val(k, querydict[k]),[k for k in querydict])) | ||
102 | |||
103 | def _get_toggle_order(request, orderkey): | ||
104 | return "%s:-" % orderkey if request.GET.get('orderby', "") == "%s:+" % orderkey else "%s:+" % orderkey | ||
105 | |||
106 | # we check that the input comes in a valid form that we can recognize | ||
107 | def _validate_input(input, model): | ||
108 | |||
109 | invalid = None | ||
110 | |||
111 | if input: | ||
112 | input_list = input.split(FIELD_SEPARATOR) | ||
113 | |||
114 | # Check we have only one colon | ||
115 | if len(input_list) != 2: | ||
116 | invalid = "We have an invalid number of separators" | ||
117 | return None, invalid | ||
118 | |||
119 | # Check we have an equal number of terms both sides of the colon | ||
120 | if len(input_list[0].split(VALUE_SEPARATOR)) != len(input_list[1].split(VALUE_SEPARATOR)): | ||
121 | invalid = "Not all arg names got values" | ||
122 | return None, invalid + str(input_list) | ||
123 | |||
124 | # Check we are looking for a valid field | ||
125 | valid_fields = model._meta.get_all_field_names() | ||
126 | for field in input_list[0].split(VALUE_SEPARATOR): | ||
127 | if not reduce(lambda x, y: x or y, map(lambda x: field.startswith(x), [ x for x in valid_fields ])): | ||
128 | return None, (field, [ x for x in valid_fields ]) | ||
129 | |||
130 | return input, invalid | ||
131 | |||
132 | # uses search_allowed_fields in orm/models.py to create a search query | ||
133 | # for these fields with the supplied input text | ||
134 | def _get_search_results(search_term, queryset, model): | ||
135 | search_objects = [] | ||
136 | for st in search_term.split(" "): | ||
137 | q_map = map(lambda x: Q(**{x+'__icontains': st}), | ||
138 | model.search_allowed_fields) | ||
139 | |||
140 | search_objects.append(reduce(operator.or_, q_map)) | ||
141 | search_object = reduce(operator.and_, search_objects) | ||
142 | queryset = queryset.filter(search_object) | ||
143 | |||
144 | return queryset | ||
145 | |||
146 | |||
147 | # function to extract the search/filter/ordering parameters from the request | ||
148 | # it uses the request and the model to validate input for the filter and orderby values | ||
149 | def _search_tuple(request, model): | ||
150 | ordering_string, invalid = _validate_input(request.GET.get('orderby', ''), model) | ||
151 | if invalid: | ||
152 | raise BaseException("Invalid ordering " + str(invalid)) | ||
153 | |||
154 | filter_string, invalid = _validate_input(request.GET.get('filter', ''), model) | ||
155 | if invalid: | ||
156 | raise BaseException("Invalid filter " + str(invalid)) | ||
157 | |||
158 | search_term = request.GET.get('search', '') | ||
159 | return (filter_string, search_term, ordering_string) | ||
160 | |||
161 | |||
162 | # returns a lazy-evaluated queryset for a filter/search/order combination | ||
163 | def _get_queryset(model, filter_string, search_term, ordering_string): | ||
164 | if filter_string: | ||
165 | filter_query = _get_filtering_query(filter_string) | ||
166 | queryset = model.objects.filter(filter_query) | ||
167 | else: | ||
168 | queryset = model.objects.all() | ||
169 | |||
170 | if search_term: | ||
171 | queryset = _get_search_results(search_term, queryset, model) | ||
172 | |||
173 | if ordering_string and queryset: | ||
174 | column, order = ordering_string.split(':') | ||
175 | if order.lower() == DESCENDING: | ||
176 | queryset = queryset.order_by('-' + column) | ||
177 | else: | ||
178 | queryset = queryset.order_by(column) | ||
179 | |||
180 | return queryset | ||
75 | 181 | ||
76 | # shows the "all builds" page | 182 | # shows the "all builds" page |
77 | def builds(request): | 183 | def builds(request): |
@@ -84,16 +190,24 @@ def builds(request): | |||
84 | if retval: | 190 | if retval: |
85 | return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters) | 191 | return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters) |
86 | 192 | ||
87 | # retrieve the objects that will be displayed in the table | 193 | # boilerplate code that takes a request for an object type and returns a queryset |
88 | build_info = _build_page_range(Paginator(Build.objects.exclude(outcome = Build.IN_PROGRESS).order_by("-id"), request.GET.get('count', 10)),request.GET.get('page', 1)) | 194 | # for that object type. copypasta for all needed table searches |
195 | (filter_string, search_term, ordering_string) = _search_tuple(request, Build) | ||
196 | queryset = _get_queryset(Build, filter_string, search_term, ordering_string) | ||
197 | |||
198 | # retrieve the objects that will be displayed in the table; builds a paginator and gets a page range to display | ||
199 | build_info = _build_page_range(Paginator(queryset.exclude(outcome = Build.IN_PROGRESS), request.GET.get('count', 10)),request.GET.get('page', 1)) | ||
89 | 200 | ||
90 | # build view-specific information; this is rendered specifically in the builds page | 201 | # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds) |
91 | build_mru = Build.objects.order_by("-started_on")[:3] | 202 | build_mru = Build.objects.filter(completed_on__gte=(timezone.now()-timedelta(hours=24))).order_by("-started_on")[:3] |
92 | for b in [ x for x in build_mru if x.outcome == Build.IN_PROGRESS ]: | 203 | for b in [ x for x in build_mru if x.outcome == Build.IN_PROGRESS ]: |
93 | tf = Task.objects.filter(build = b) | 204 | tf = Task.objects.filter(build = b) |
94 | b.completeper = tf.exclude(order__isnull=True).count()*100/tf.count() | 205 | b.completeper = tf.exclude(order__isnull=True).count()*100/tf.count() |
95 | from django.utils import timezone | 206 | b.eta = timezone.now() |
96 | b.eta = timezone.now() + ((timezone.now() - b.started_on)*100/b.completeper) | 207 | if b.completeper > 0: |
208 | b.eta += ((timezone.now() - b.started_on)*100/b.completeper) | ||
209 | else: | ||
210 | b.eta = 0 | ||
97 | 211 | ||
98 | # send the data to the template | 212 | # send the data to the template |
99 | context = { | 213 | context = { |
@@ -101,19 +215,78 @@ def builds(request): | |||
101 | 'mru' : build_mru, | 215 | 'mru' : build_mru, |
102 | # TODO: common objects for all table views, adapt as needed | 216 | # TODO: common objects for all table views, adapt as needed |
103 | 'objects' : build_info, | 217 | 'objects' : build_info, |
218 | # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns | ||
104 | 'tablecols' : [ | 219 | 'tablecols' : [ |
105 | {'name': 'Target ', 'clclass': 'target',}, | 220 | {'name': 'Outcome ', # column with a single filter |
106 | {'name': 'Machine ', 'clclass': 'machine'}, | 221 | 'qhelp' : "The outcome tells you if a build completed successfully or failed", # the help button content |
107 | {'name': 'Completed on ', 'clclass': 'completed_on'}, | 222 | 'dclass' : "span2", # indication about column width; comes from the design |
108 | {'name': 'Failed tasks ', 'clclass': 'failed_tasks'}, | 223 | 'orderfield': _get_toggle_order(request, "outcome"), # adds ordering by the field value; default ascending unless clicked from ascending into descending |
109 | {'name': 'Errors ', 'clclass': 'errors_no'}, | 224 | # filter field will set a filter on that column with the specs in the filter description |
110 | {'name': 'Warnings', 'clclass': 'warnings_no'}, | 225 | # the class field in the filter has no relation with clclass; the control different aspects of the UI |
111 | {'name': 'Output ', 'clclass': 'output'}, | 226 | # still, it is recommended for the values to be identical for easy tracking in the generated HTML |
112 | {'name': 'Started on ', 'clclass': 'started_on', 'hidden' : 1}, | 227 | 'filter' : {'class' : 'outcome', 'label': 'Show only', 'options' : { |
113 | {'name': 'Time ', 'clclass': 'time', 'hidden' : 1}, | 228 | 'Successful builds': 'outcome:' + str(Build.SUCCEEDED), # this is the field search expression |
114 | {'name': 'Output', 'clclass': 'output'}, | 229 | 'Failed builds': 'outcome:'+ str(Build.FAILED), |
115 | {'name': 'Log', 'clclass': 'log', 'hidden': 1}, | 230 | } |
116 | ]} | 231 | } |
232 | }, | ||
233 | {'name': 'Target ', # default column, disabled box, with just the name in the list | ||
234 | 'qhelp': "This is the build target(s): one or more recipes or image recipes", | ||
235 | 'orderfield': _get_toggle_order(request, "target__target"), | ||
236 | }, | ||
237 | {'name': 'Machine ', | ||
238 | 'qhelp': "The machine is the hardware for which you are building", | ||
239 | 'dclass': 'span3'}, # a slightly wider column | ||
240 | {'name': 'Started on ', 'clclass': 'started_on', 'hidden' : 1, # this is an unchecked box, which hides the column | ||
241 | 'qhelp': "The date and time you started the build", | ||
242 | 'filter' : {'class' : 'started_on', 'label': 'Show only builds started', 'options' : { | ||
243 | 'Today' : 'started_on__gte:'+timezone.now().strftime("%Y-%m-%d"), | ||
244 | 'Yesterday' : 'started_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), | ||
245 | 'Within one week' : 'started_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), | ||
246 | }} | ||
247 | }, | ||
248 | {'name': 'Completed on ', | ||
249 | 'qhelp': "The date and time the build finished", | ||
250 | 'orderfield': _get_toggle_order(request, "completed_on"), | ||
251 | 'filter' : {'class' : 'completed_on', 'label': 'Show only builds completed', 'options' : { | ||
252 | 'Today' : 'completed_on__gte:'+timezone.now().strftime("%Y-%m-%d"), | ||
253 | 'Yesterday' : 'completed_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), | ||
254 | 'Within one week' : 'completed_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), | ||
255 | }} | ||
256 | }, | ||
257 | {'name': 'Failed tasks ', 'clclass': 'failed_tasks', # specifing a clclass will enable the checkbox | ||
258 | 'qhelp': "How many tasks failed during the build", | ||
259 | 'filter' : {'class' : 'failed_tasks', 'label': 'Show only ', 'options' : { | ||
260 | 'Builds with failed tasks' : 'task_build__outcome:4', | ||
261 | 'Builds without failed tasks' : 'task_build__outcome:NOT4', | ||
262 | }} | ||
263 | }, | ||
264 | {'name': 'Errors ', 'clclass': 'errors_no', | ||
265 | 'qhelp': "How many errors were encountered during the build (if any)", | ||
266 | 'orderfield': _get_toggle_order(request, "errors_no"), | ||
267 | 'filter' : {'class' : 'errors_no', 'label': 'Show only ', 'options' : { | ||
268 | 'Builds with errors' : 'errors_no__gte:1', | ||
269 | 'Builds without errors' : 'errors_no:0', | ||
270 | }} | ||
271 | }, | ||
272 | {'name': 'Warnings', 'clclass': 'warnings_no', | ||
273 | 'qhelp': "How many warnigns were encountered during the build (if any)", | ||
274 | 'orderfield': _get_toggle_order(request, "warnings_no"), | ||
275 | 'filter' : {'class' : 'warnings_no', 'label': 'Show only ', 'options' : { | ||
276 | 'Builds with warnings' : 'warnings_no__gte:1', | ||
277 | 'Builds without warnings' : 'warnings_no:0', | ||
278 | }} | ||
279 | }, | ||
280 | {'name': 'Time ', 'clclass': 'time', 'hidden' : 1, | ||
281 | 'qhelp': "How long it took the build to finish",}, | ||
282 | {'name': 'Log', | ||
283 | 'dclass': "span4", | ||
284 | 'qhelp': "The location in disk of the build main log file", | ||
285 | 'clclass': 'log', 'hidden': 1}, | ||
286 | {'name': 'Output', 'clclass': 'output', | ||
287 | 'qhelp': "The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory"}, | ||
288 | ] | ||
289 | } | ||
117 | 290 | ||
118 | return render(request, template, context) | 291 | return render(request, template, context) |
119 | 292 | ||
@@ -191,8 +364,10 @@ def tasks(request, build_id): | |||
191 | retval = _verify_parameters( request.GET, mandatory_parameters ) | 364 | retval = _verify_parameters( request.GET, mandatory_parameters ) |
192 | if retval: | 365 | if retval: |
193 | return _redirect_parameters( 'tasks', request.GET, mandatory_parameters, build_id = build_id) | 366 | return _redirect_parameters( 'tasks', request.GET, mandatory_parameters, build_id = build_id) |
367 | (filter_string, search_term, ordering_string) = _search_tuple(request, Task) | ||
368 | queryset = _get_queryset(Task, filter_string, search_term, ordering_string) | ||
194 | 369 | ||
195 | tasks = _build_page_range(Paginator(Task.objects.filter(build=build_id, order__gt=0), request.GET.get('count', 100)),request.GET.get('page', 1)) | 370 | tasks = _build_page_range(Paginator(queryset.filter(build=build_id, order__gt=0), request.GET.get('count', 100)),request.GET.get('page', 1)) |
196 | 371 | ||
197 | for t in tasks: | 372 | for t in tasks: |
198 | if t.outcome == Task.OUTCOME_COVERED: | 373 | if t.outcome == Task.OUTCOME_COVERED: |
@@ -208,8 +383,10 @@ def recipes(request, build_id): | |||
208 | retval = _verify_parameters( request.GET, mandatory_parameters ) | 383 | retval = _verify_parameters( request.GET, mandatory_parameters ) |
209 | if retval: | 384 | if retval: |
210 | return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id) | 385 | return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id) |
386 | (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe) | ||
387 | queryset = _get_queryset(Recipe, filter_string, search_term, ordering_string) | ||
211 | 388 | ||
212 | recipes = _build_page_range(Paginator(Recipe.objects.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id)), request.GET.get('count', 100)),request.GET.get('page', 1)) | 389 | recipes = _build_page_range(Paginator(queryset.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id)), request.GET.get('count', 100)),request.GET.get('page', 1)) |
213 | 390 | ||
214 | context = {'build': Build.objects.filter(pk=build_id)[0], 'objects': recipes, } | 391 | context = {'build': Build.objects.filter(pk=build_id)[0], 'objects': recipes, } |
215 | 392 | ||
@@ -218,15 +395,63 @@ def recipes(request, build_id): | |||
218 | 395 | ||
219 | def configuration(request, build_id): | 396 | def configuration(request, build_id): |
220 | template = 'configuration.html' | 397 | template = 'configuration.html' |
398 | context = {'build': Build.objects.filter(pk=build_id)[0]} | ||
399 | return render(request, template, context) | ||
400 | |||
401 | |||
402 | def configvars(request, build_id): | ||
403 | template = 'configvars.html' | ||
221 | mandatory_parameters = { 'count': 100, 'page' : 1}; | 404 | mandatory_parameters = { 'count': 100, 'page' : 1}; |
222 | retval = _verify_parameters( request.GET, mandatory_parameters ) | 405 | retval = _verify_parameters( request.GET, mandatory_parameters ) |
223 | if retval: | 406 | if retval: |
224 | return _redirect_parameters( 'configuration', request.GET, mandatory_parameters, build_id = build_id) | 407 | return _redirect_parameters( 'configvars', request.GET, mandatory_parameters, build_id = build_id) |
408 | |||
409 | (filter_string, search_term, ordering_string) = _search_tuple(request, Variable) | ||
410 | queryset = _get_queryset(Variable, filter_string, search_term, ordering_string) | ||
411 | |||
412 | variables = _build_page_range(Paginator(queryset.filter(build=build_id), request.GET.get('count', 50)), request.GET.get('page', 1)) | ||
413 | |||
414 | context = { | ||
415 | 'build': Build.objects.filter(pk=build_id)[0], | ||
416 | 'objects' : variables, | ||
417 | # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns | ||
418 | 'tablecols' : [ | ||
419 | {'name': 'Variable ', | ||
420 | 'qhelp': "Base variable expanded name", | ||
421 | 'clclass' : 'variable', | ||
422 | 'dclass' : "span3", | ||
423 | 'orderfield': _get_toggle_order(request, "variable_name"), | ||
424 | }, | ||
425 | {'name': 'Value ', | ||
426 | 'qhelp': "The value assigned to the variable", | ||
427 | 'clclass': 'variable_value', | ||
428 | 'dclass': "span4", | ||
429 | 'orderfield': _get_toggle_order(request, "variable_value"), | ||
430 | }, | ||
431 | {'name': 'Configuration file(s) ', | ||
432 | 'qhelp': "The configuration file(s) that touched the variable value", | ||
433 | 'clclass': 'file', | ||
434 | 'dclass': "span6", | ||
435 | 'orderfield': _get_toggle_order(request, "variable_vhistory__file_name"), | ||
436 | 'filter' : { 'class': 'file', 'label' : 'Show only', 'options' : { | ||
437 | } | ||
438 | } | ||
439 | }, | ||
440 | {'name': 'Description ', | ||
441 | 'qhelp': "A brief explanation of a variable", | ||
442 | 'clclass': 'description', | ||
443 | 'dclass': "span5", | ||
444 | 'orderfield': _get_toggle_order(request, "description"), | ||
445 | 'filter' : { 'class' : 'description', 'label' : 'No', 'options' : { | ||
446 | } | ||
447 | }, | ||
448 | } | ||
449 | ] | ||
450 | } | ||
225 | 451 | ||
226 | variables = _build_page_range(Paginator(Variable.objects.filter(build=build_id), 50), request.GET.get('page', 1)) | ||
227 | context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : variables} | ||
228 | return render(request, template, context) | 452 | return render(request, template, context) |
229 | 453 | ||
454 | |||
230 | def buildtime(request, build_id): | 455 | def buildtime(request, build_id): |
231 | template = "buildtime.html" | 456 | template = "buildtime.html" |
232 | if Build.objects.filter(pk=build_id).count() == 0 : | 457 | if Build.objects.filter(pk=build_id).count() == 0 : |
@@ -263,8 +488,10 @@ def bpackage(request, build_id): | |||
263 | retval = _verify_parameters( request.GET, mandatory_parameters ) | 488 | retval = _verify_parameters( request.GET, mandatory_parameters ) |
264 | if retval: | 489 | if retval: |
265 | return _redirect_parameters( 'packages', request.GET, mandatory_parameters, build_id = build_id) | 490 | return _redirect_parameters( 'packages', request.GET, mandatory_parameters, build_id = build_id) |
491 | (filter_string, search_term, ordering_string) = _search_tuple(request, Package) | ||
492 | queryset = _get_queryset(Package, filter_string, search_term, ordering_string) | ||
266 | 493 | ||
267 | packages = _build_page_range(Paginator(Package.objects.filter(build = build_id), request.GET.get('count', 100)),request.GET.get('page', 1)) | 494 | packages = _build_page_range(Paginator(queryset.filter(build = build_id), request.GET.get('count', 100)),request.GET.get('page', 1)) |
268 | 495 | ||
269 | context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : packages} | 496 | context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : packages} |
270 | return render(request, template, context) | 497 | return render(request, template, context) |
@@ -305,139 +532,4 @@ def layer_versions_recipes(request, layerversion_id): | |||
305 | 532 | ||
306 | return render(request, template, context) | 533 | return render(request, template, context) |
307 | 534 | ||
308 | #### API | ||
309 | |||
310 | import json | ||
311 | from django.core import serializers | ||
312 | from django.http import HttpResponse, HttpResponseBadRequest | ||
313 | |||
314 | |||
315 | def model_explorer(request, model_name): | ||
316 | |||
317 | DESCENDING = 'desc' | ||
318 | response_data = {} | ||
319 | model_mapping = { | ||
320 | 'build': Build, | ||
321 | 'target': Target, | ||
322 | 'task': Task, | ||
323 | 'task_dependency': Task_Dependency, | ||
324 | 'package': Package, | ||
325 | 'layer': Layer, | ||
326 | 'layerversion': Layer_Version, | ||
327 | 'recipe': Recipe, | ||
328 | 'recipe_dependency': Recipe_Dependency, | ||
329 | 'package': Package, | ||
330 | 'package_dependency': Package_Dependency, | ||
331 | 'build_file': Package_File, | ||
332 | 'variable': Variable, | ||
333 | 'logmessage': LogMessage, | ||
334 | } | ||
335 | |||
336 | if model_name not in model_mapping.keys(): | ||
337 | return HttpResponseBadRequest() | ||
338 | |||
339 | model = model_mapping[model_name] | ||
340 | |||
341 | try: | ||
342 | limit = int(request.GET.get('limit', 0)) | ||
343 | except ValueError: | ||
344 | limit = 0 | ||
345 | |||
346 | try: | ||
347 | offset = int(request.GET.get('offset', 0)) | ||
348 | except ValueError: | ||
349 | offset = 0 | ||
350 | |||
351 | ordering_string, invalid = _validate_input(request.GET.get('orderby', ''), | ||
352 | model) | ||
353 | if invalid: | ||
354 | return HttpResponseBadRequest() | ||
355 | |||
356 | filter_string, invalid = _validate_input(request.GET.get('filter', ''), | ||
357 | model) | ||
358 | if invalid: | ||
359 | return HttpResponseBadRequest() | ||
360 | |||
361 | search_term = request.GET.get('search', '') | ||
362 | |||
363 | if filter_string: | ||
364 | filter_terms = _get_filtering_terms(filter_string) | ||
365 | try: | ||
366 | queryset = model.objects.filter(**filter_terms) | ||
367 | except ValueError: | ||
368 | queryset = [] | ||
369 | else: | ||
370 | queryset = model.objects.all() | ||
371 | 535 | ||
372 | if search_term: | ||
373 | queryset = _get_search_results(search_term, queryset, model) | ||
374 | |||
375 | if ordering_string and queryset: | ||
376 | column, order = ordering_string.split(':') | ||
377 | if order.lower() == DESCENDING: | ||
378 | queryset = queryset.order_by('-' + column) | ||
379 | else: | ||
380 | queryset = queryset.order_by(column) | ||
381 | |||
382 | if offset and limit: | ||
383 | queryset = queryset[offset:(offset+limit)] | ||
384 | elif offset: | ||
385 | queryset = queryset[offset:] | ||
386 | elif limit: | ||
387 | queryset = queryset[:limit] | ||
388 | |||
389 | if queryset: | ||
390 | response_data['count'] = queryset.count() | ||
391 | else: | ||
392 | response_data['count'] = 0 | ||
393 | response_data['list'] = serializers.serialize('json', queryset) | ||
394 | # response_data = serializers.serialize('json', queryset) | ||
395 | |||
396 | return HttpResponse(json.dumps(response_data), | ||
397 | content_type='application/json') | ||
398 | |||
399 | def _get_filtering_terms(filter_string): | ||
400 | |||
401 | search_terms = filter_string.split(":") | ||
402 | keys = search_terms[0].split(',') | ||
403 | values = search_terms[1].split(',') | ||
404 | |||
405 | return dict(zip(keys, values)) | ||
406 | |||
407 | def _validate_input(input, model): | ||
408 | |||
409 | invalid = 0 | ||
410 | |||
411 | if input: | ||
412 | input_list = input.split(":") | ||
413 | |||
414 | # Check we have only one colon | ||
415 | if len(input_list) != 2: | ||
416 | invalid = 1 | ||
417 | return None, invalid | ||
418 | |||
419 | # Check we have an equal number of terms both sides of the colon | ||
420 | if len(input_list[0].split(',')) != len(input_list[1].split(',')): | ||
421 | invalid = 1 | ||
422 | return None, invalid | ||
423 | |||
424 | # Check we are looking for a valid field | ||
425 | valid_fields = model._meta.get_all_field_names() | ||
426 | for field in input_list[0].split(','): | ||
427 | if field not in valid_fields: | ||
428 | invalid = 1 | ||
429 | return None, invalid | ||
430 | |||
431 | return input, invalid | ||
432 | |||
433 | def _get_search_results(search_term, queryset, model): | ||
434 | search_objects = [] | ||
435 | for st in search_term.split(" "): | ||
436 | q_map = map(lambda x: Q(**{x+'__icontains': st}), | ||
437 | model.search_allowed_fields) | ||
438 | |||
439 | search_objects.append(reduce(operator.or_, q_map)) | ||
440 | search_object = reduce(operator.and_, search_objects) | ||
441 | queryset = queryset.filter(search_object) | ||
442 | |||
443 | return queryset | ||