summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/toaster/toastergui/views.py
diff options
context:
space:
mode:
Diffstat (limited to 'bitbake/lib/toaster/toastergui/views.py')
-rw-r--r--bitbake/lib/toaster/toastergui/views.py1645
1 files changed, 1645 insertions, 0 deletions
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
new file mode 100644
index 0000000..e4ada14
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -0,0 +1,1645 @@
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) 2013 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
22import operator,re
23
24from django.db.models import Q, Sum
25from django.shortcuts import render, redirect
26from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe, LogMessage, Variable
27from orm.models import Task_Dependency, Recipe_Dependency, Package, Package_File, Package_Dependency
28from orm.models import Target_Installed_Package, Target_File, Target_Image_File
29from django.views.decorators.cache import cache_control
30from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
31from django.http import HttpResponseBadRequest
32from django.utils import timezone
33from datetime import timedelta
34from django.utils import formats
35import json
36
37def _build_page_range(paginator, index = 1):
38 try:
39 page = paginator.page(index)
40 except PageNotAnInteger:
41 page = paginator.page(1)
42 except EmptyPage:
43 page = paginator.page(paginator.num_pages)
44
45
46 page.page_range = [page.number]
47 crt_range = 0
48 for i in range(1,5):
49 if (page.number + i) <= paginator.num_pages:
50 page.page_range = page.page_range + [ page.number + i]
51 crt_range +=1
52 if (page.number - i) > 0:
53 page.page_range = [page.number -i] + page.page_range
54 crt_range +=1
55 if crt_range == 4:
56 break
57 return page
58
59
60def _verify_parameters(g, mandatory_parameters):
61 miss = []
62 for mp in mandatory_parameters:
63 if not mp in g:
64 miss.append(mp)
65 if len(miss):
66 return miss
67 return None
68
69def _redirect_parameters(view, g, mandatory_parameters, *args, **kwargs):
70 import urllib
71 from django.core.urlresolvers import reverse
72 url = reverse(view, kwargs=kwargs)
73 params = {}
74 for i in g:
75 params[i] = g[i]
76 for i in mandatory_parameters:
77 if not i in params:
78 params[i] = mandatory_parameters[i]
79
80 return redirect(url + "?%s" % urllib.urlencode(params), *args, **kwargs)
81
82FIELD_SEPARATOR = ":"
83VALUE_SEPARATOR = "!"
84DESCENDING = "-"
85
86def __get_q_for_val(name, value):
87 if "OR" in value:
88 return reduce(operator.or_, map(lambda x: __get_q_for_val(name, x), [ x for x in value.split("OR") ]))
89 if "AND" in value:
90 return reduce(operator.and_, map(lambda x: __get_q_for_val(name, x), [ x for x in value.split("AND") ]))
91 if value.startswith("NOT"):
92 kwargs = { name : value.strip("NOT") }
93 return ~Q(**kwargs)
94 else:
95 kwargs = { name : value }
96 return Q(**kwargs)
97
98def _get_filtering_query(filter_string):
99
100 search_terms = filter_string.split(FIELD_SEPARATOR)
101 keys = search_terms[0].split(VALUE_SEPARATOR)
102 values = search_terms[1].split(VALUE_SEPARATOR)
103
104 querydict = dict(zip(keys, values))
105 return reduce(operator.and_, map(lambda x: __get_q_for_val(x, querydict[x]), [k for k in querydict]))
106
107def _get_toggle_order(request, orderkey, reverse = False):
108 if reverse:
109 return "%s:+" % orderkey if request.GET.get('orderby', "") == "%s:-" % orderkey else "%s:-" % orderkey
110 else:
111 return "%s:-" % orderkey if request.GET.get('orderby', "") == "%s:+" % orderkey else "%s:+" % orderkey
112
113def _get_toggle_order_icon(request, orderkey):
114 if request.GET.get('orderby', "") == "%s:+"%orderkey:
115 return "down"
116 elif request.GET.get('orderby', "") == "%s:-"%orderkey:
117 return "up"
118 else:
119 return None
120
121# we check that the input comes in a valid form that we can recognize
122def _validate_input(input, model):
123
124 invalid = None
125
126 if input:
127 input_list = input.split(FIELD_SEPARATOR)
128
129 # Check we have only one colon
130 if len(input_list) != 2:
131 invalid = "We have an invalid number of separators: " + input + " -> " + str(input_list)
132 return None, invalid
133
134 # Check we have an equal number of terms both sides of the colon
135 if len(input_list[0].split(VALUE_SEPARATOR)) != len(input_list[1].split(VALUE_SEPARATOR)):
136 invalid = "Not all arg names got values"
137 return None, invalid + str(input_list)
138
139 # Check we are looking for a valid field
140 valid_fields = model._meta.get_all_field_names()
141 for field in input_list[0].split(VALUE_SEPARATOR):
142 if not reduce(lambda x, y: x or y, map(lambda x: field.startswith(x), [ x for x in valid_fields ])):
143 return None, (field, [ x for x in valid_fields ])
144
145 return input, invalid
146
147# uses search_allowed_fields in orm/models.py to create a search query
148# for these fields with the supplied input text
149def _get_search_results(search_term, queryset, model):
150 search_objects = []
151 for st in search_term.split(" "):
152 q_map = map(lambda x: Q(**{x+'__icontains': st}),
153 model.search_allowed_fields)
154
155 search_objects.append(reduce(operator.or_, q_map))
156 search_object = reduce(operator.and_, search_objects)
157 queryset = queryset.filter(search_object)
158
159 return queryset
160
161
162# function to extract the search/filter/ordering parameters from the request
163# it uses the request and the model to validate input for the filter and orderby values
164def _search_tuple(request, model):
165 ordering_string, invalid = _validate_input(request.GET.get('orderby', ''), model)
166 if invalid:
167 raise BaseException("Invalid ordering model:" + str(model) + str(invalid))
168
169 filter_string, invalid = _validate_input(request.GET.get('filter', ''), model)
170 if invalid:
171 raise BaseException("Invalid filter " + str(invalid))
172
173 search_term = request.GET.get('search', '')
174 return (filter_string, search_term, ordering_string)
175
176
177# returns a lazy-evaluated queryset for a filter/search/order combination
178def _get_queryset(model, queryset, filter_string, search_term, ordering_string, ordering_secondary=''):
179 if filter_string:
180 filter_query = _get_filtering_query(filter_string)
181 queryset = queryset.filter(filter_query)
182 else:
183 queryset = queryset.all()
184
185 if search_term:
186 queryset = _get_search_results(search_term, queryset, model)
187
188 if ordering_string and queryset:
189 column, order = ordering_string.split(':')
190 if column == re.sub('-','',ordering_secondary):
191 ordering_secondary=''
192 if order.lower() == DESCENDING:
193 column = '-' + column
194 if ordering_secondary:
195 queryset = queryset.order_by(column, ordering_secondary)
196 else:
197 queryset = queryset.order_by(column)
198
199 # insure only distinct records (e.g. from multiple search hits) are returned
200 return queryset.distinct()
201
202
203# shows the "all builds" page
204def builds(request):
205 template = 'build.html'
206 # define here what parameters the view needs in the GET portion in order to
207 # be able to display something. 'count' and 'page' are mandatory for all views
208 # that use paginators.
209 mandatory_parameters = { 'count': 10, 'page' : 1, 'orderby' : 'completed_on:-' };
210 retval = _verify_parameters( request.GET, mandatory_parameters )
211 if retval:
212 return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters)
213
214 # boilerplate code that takes a request for an object type and returns a queryset
215 # for that object type. copypasta for all needed table searches
216 (filter_string, search_term, ordering_string) = _search_tuple(request, Build)
217 queryset_all = Build.objects.exclude(outcome = Build.IN_PROGRESS)
218 queryset_with_search = _get_queryset(Build, queryset_all, None, search_term, ordering_string, '-completed_on')
219 queryset = _get_queryset(Build, queryset_all, filter_string, search_term, ordering_string, '-completed_on')
220
221 # retrieve the objects that will be displayed in the table; builds a paginator and gets a page range to display
222 build_info = _build_page_range(Paginator(queryset, request.GET.get('count', 10)),request.GET.get('page', 1))
223
224 # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds)
225 build_mru = Build.objects.filter(completed_on__gte=(timezone.now()-timedelta(hours=24))).order_by("-started_on")[:3]
226 for b in [ x for x in build_mru if x.outcome == Build.IN_PROGRESS ]:
227 tf = Task.objects.filter(build = b)
228 tfc = tf.count()
229 if tfc > 0:
230 b.completeper = tf.exclude(order__isnull=True).count()*100/tf.count()
231 else:
232 b.completeper = 0
233 b.eta = timezone.now()
234 if b.completeper > 0:
235 b.eta += ((timezone.now() - b.started_on)*100/b.completeper)
236 else:
237 b.eta = 0
238
239 # set up list of fstypes for each build
240 fstypes_map = {};
241 for build in build_info:
242 targets = Target.objects.filter( build_id = build.id )
243 comma = "";
244 extensions = "";
245 for t in targets:
246 if ( not t.is_image ):
247 continue
248 tif = Target_Image_File.objects.filter( target_id = t.id )
249 for i in tif:
250 s=re.sub('.*tar.bz2', 'tar.bz2', i.file_name)
251 if s == i.file_name:
252 s=re.sub('.*\.', '', i.file_name)
253 if None == re.search(s,extensions):
254 extensions += comma + s
255 comma = ", "
256 fstypes_map[build.id]=extensions
257
258 # send the data to the template
259 context = {
260 # specific info for
261 'mru' : build_mru,
262 # TODO: common objects for all table views, adapt as needed
263 'objects' : build_info,
264 'objectname' : "builds",
265 'fstypes' : fstypes_map,
266 'search_term' : search_term,
267 'total_count' : queryset_with_search.count(),
268 # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
269 'tablecols' : [
270 {'name': 'Outcome', # column with a single filter
271 'qhelp' : "The outcome tells you if a build successfully completed or failed", # the help button content
272 'dclass' : "span2", # indication about column width; comes from the design
273 'orderfield': _get_toggle_order(request, "outcome"), # adds ordering by the field value; default ascending unless clicked from ascending into descending
274 'ordericon':_get_toggle_order_icon(request, "outcome"),
275 # filter field will set a filter on that column with the specs in the filter description
276 # the class field in the filter has no relation with clclass; the control different aspects of the UI
277 # still, it is recommended for the values to be identical for easy tracking in the generated HTML
278 'filter' : {'class' : 'outcome',
279 'label': 'Show:',
280 'options' : [
281 ('Successful builds', 'outcome:' + str(Build.SUCCEEDED), queryset_with_search.filter(outcome=str(Build.SUCCEEDED)).count()), # this is the field search expression
282 ('Failed builds', 'outcome:'+ str(Build.FAILED), queryset_with_search.filter(outcome=str(Build.FAILED)).count()),
283 ]
284 }
285 },
286 {'name': 'Target', # default column, disabled box, with just the name in the list
287 'qhelp': "This is the build target or build targets (i.e. one or more recipes or image recipes)",
288 'orderfield': _get_toggle_order(request, "target__target"),
289 'ordericon':_get_toggle_order_icon(request, "target__target"),
290 },
291 {'name': 'Machine',
292 'qhelp': "The machine is the hardware for which you are building a recipe or image recipe",
293 'orderfield': _get_toggle_order(request, "machine"),
294 'ordericon':_get_toggle_order_icon(request, "machine"),
295 'dclass': 'span3'
296 }, # a slightly wider column
297 {'name': 'Started on', 'clclass': 'started_on', 'hidden' : 1, # this is an unchecked box, which hides the column
298 'qhelp': "The date and time you started the build",
299 'orderfield': _get_toggle_order(request, "started_on", True),
300 'ordericon':_get_toggle_order_icon(request, "started_on"),
301 'filter' : {'class' : 'started_on',
302 'label': 'Show:',
303 'options' : [
304 ("Today's builds" , 'started_on__gte:'+timezone.now().strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=timezone.now().strftime("%Y-%m-%d")).count()),
305 ("Yesterday's builds", 'started_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d")).count()),
306 ("This week's builds", 'started_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d")).count()),
307 ]
308 }
309 },
310 {'name': 'Completed on',
311 'qhelp': "The date and time the build finished",
312 'orderfield': _get_toggle_order(request, "completed_on", True),
313 'ordericon':_get_toggle_order_icon(request, "completed_on"),
314 'filter' : {'class' : 'completed_on',
315 'label': 'Show:',
316 'options' : [
317 ("Today's builds", 'completed_on__gte:'+timezone.now().strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=timezone.now().strftime("%Y-%m-%d")).count()),
318 ("Yesterday's builds", 'completed_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d")).count()),
319 ("This week's builds", 'completed_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d")).count()),
320 ]
321 }
322 },
323 {'name': 'Failed tasks', 'clclass': 'failed_tasks', # specifing a clclass will enable the checkbox
324 'qhelp': "How many tasks failed during the build",
325 'filter' : {'class' : 'failed_tasks',
326 'label': 'Show:',
327 'options' : [
328 ('Builds with failed tasks', 'task_build__outcome:4', queryset_with_search.filter(task_build__outcome=4).count()),
329 ('Builds without failed tasks', 'task_build__outcome:NOT4', queryset_with_search.filter(~Q(task_build__outcome=4)).count()),
330 ]
331 }
332 },
333 {'name': 'Errors', 'clclass': 'errors_no',
334 'qhelp': "How many errors were encountered during the build (if any)",
335 'orderfield': _get_toggle_order(request, "errors_no", True),
336 'ordericon':_get_toggle_order_icon(request, "errors_no"),
337 'filter' : {'class' : 'errors_no',
338 'label': 'Show:',
339 'options' : [
340 ('Builds with errors', 'errors_no__gte:1', queryset_with_search.filter(errors_no__gte=1).count()),
341 ('Builds without errors', 'errors_no:0', queryset_with_search.filter(errors_no=0).count()),
342 ]
343 }
344 },
345 {'name': 'Warnings', 'clclass': 'warnings_no',
346 'qhelp': "How many warnings were encountered during the build (if any)",
347 'orderfield': _get_toggle_order(request, "warnings_no", True),
348 'ordericon':_get_toggle_order_icon(request, "warnings_no"),
349 'filter' : {'class' : 'warnings_no',
350 'label': 'Show:',
351 'options' : [
352 ('Builds with warnings','warnings_no__gte:1', queryset_with_search.filter(warnings_no__gte=1).count()),
353 ('Builds without warnings','warnings_no:0', queryset_with_search.filter(warnings_no=0).count()),
354 ]
355 }
356 },
357 {'name': 'Time', 'clclass': 'time', 'hidden' : 1,
358 'qhelp': "How long it took the build to finish",
359 'orderfield': _get_toggle_order(request, "timespent", True),
360 'ordericon':_get_toggle_order_icon(request, "timespent"),
361 },
362 {'name': 'Log',
363 'dclass': "span4",
364 'qhelp': "Path to the build main log file",
365 'clclass': 'log', 'hidden': 1,
366 'orderfield': _get_toggle_order(request, "cooker_log_path"),
367 'ordericon':_get_toggle_order_icon(request, "cooker_log_path"),
368 },
369 {'name': 'Output', 'clclass': 'output',
370 'qhelp': "The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory",
371 # TODO: compute image fstypes from Target_Image_File
372 },
373 ]
374 }
375
376 return render(request, template, context)
377
378
379##
380# build dashboard for a single build, coming in as argument
381# Each build may contain multiple targets and each target
382# may generate multiple image files. display them all.
383#
384def builddashboard( request, build_id ):
385 template = "builddashboard.html"
386 if Build.objects.filter( pk=build_id ).count( ) == 0 :
387 return redirect( builds )
388 build = Build.objects.filter( pk = build_id )[ 0 ];
389 layerVersionId = Layer_Version.objects.filter( build = build_id );
390 recipeCount = Recipe.objects.filter( layer_version__id__in = layerVersionId ).count( );
391 tgts = Target.objects.filter( build_id = build_id ).order_by( 'target' );
392
393 ##
394 # set up custom target list with computed package and image data
395 #
396
397 targets = [ ]
398 ntargets = 0
399 hasImages = False
400 for t in tgts:
401 elem = { }
402 elem[ 'target' ] = t
403 if ( t.is_image ):
404 hasImages = True
405 npkg = 0
406 pkgsz = 0
407 pid= 0
408 tp = Target_Installed_Package.objects.filter( target_id = t.id )
409 package = None
410 for p in tp:
411 pid = p.package_id
412 package = Package.objects.get( pk = p.package_id )
413 pkgsz = pkgsz + package.size
414 if ( package.installed_name ):
415 npkg = npkg + 1
416 elem[ 'npkg' ] = npkg
417 elem[ 'pkgsz' ] = pkgsz
418 ti = Target_Image_File.objects.filter( target_id = t.id )
419 imageFiles = [ ]
420 for i in ti:
421 ndx = i.file_name.rfind( '/' )
422 if ( ndx < 0 ):
423 ndx = 0;
424 f = i.file_name[ ndx + 1: ]
425 imageFiles.append({ 'path': f, 'size' : i.file_size })
426 elem[ 'imageFiles' ] = imageFiles
427 targets.append( elem )
428
429 ##
430 # how many packages in this build - ignore anonymous ones
431 #
432
433 packageCount = 0
434 packages = Package.objects.filter( build_id = build_id )
435 for p in packages:
436 if ( p.installed_name ):
437 packageCount = packageCount + 1
438
439 context = {
440 'build' : build,
441 'hasImages' : hasImages,
442 'ntargets' : ntargets,
443 'targets' : targets,
444 'recipecount' : recipeCount,
445 'packagecount' : packageCount,
446 'logmessages' : LogMessage.objects.filter( build = build_id ),
447 }
448 return render( request, template, context )
449
450
451def generateCoveredList( task ):
452 revList = _find_task_revdep( task );
453 list = { };
454 for t in revList:
455 if ( t.outcome == Task.OUTCOME_COVERED ):
456 list.update( generateCoveredList( t ));
457 else:
458 list[ t.task_name ] = t;
459 return( list );
460
461def task( request, build_id, task_id ):
462 template = "task.html"
463 tasks = Task.objects.filter( pk=task_id )
464 if tasks.count( ) == 0:
465 return redirect( builds )
466 task = tasks[ 0 ];
467 dependencies = sorted(
468 _find_task_dep( task ),
469 key=lambda t:'%s_%s %s'%(t.recipe.name, t.recipe.version, t.task_name))
470 reverse_dependencies = sorted(
471 _find_task_revdep( task ),
472 key=lambda t:'%s_%s %s'%( t.recipe.name, t.recipe.version, t.task_name ))
473 coveredBy = '';
474 if ( task.outcome == Task.OUTCOME_COVERED ):
475 dict = generateCoveredList( task )
476 coveredBy = [ ]
477 for name, t in dict.items( ):
478 coveredBy.append( t )
479 log_head = ''
480 log_body = ''
481 if task.outcome == task.OUTCOME_FAILED:
482 pass
483
484 uri_list= [ ]
485 variables = Variable.objects.filter(build=build_id)
486 v=variables.filter(variable_name='SSTATE_DIR')
487 if v.count > 0:
488 uri_list.append(v[0].variable_value)
489 v=variables.filter(variable_name='SSTATE_MIRRORS')
490 if (v.count > 0):
491 for mirror in v[0].variable_value.split('\\n'):
492 s=re.sub('.* ','',mirror.strip(' \t\n\r'))
493 if len(s): uri_list.append(s)
494
495 context = {
496 'build' : Build.objects.filter( pk = build_id )[ 0 ],
497 'object' : task,
498 'task' : task,
499 'covered_by' : coveredBy,
500 'deps' : dependencies,
501 'rdeps' : reverse_dependencies,
502 'log_head' : log_head,
503 'log_body' : log_body,
504 'showing_matches' : False,
505 'uri_list' : uri_list,
506 }
507 if request.GET.get( 'show_matches', "" ):
508 context[ 'showing_matches' ] = True
509 context[ 'matching_tasks' ] = Task.objects.filter(
510 sstate_checksum=task.sstate_checksum ).filter(
511 build__completed_on__lt=task.build.completed_on).exclude(
512 order__isnull=True).exclude(outcome=Task.OUTCOME_NA).order_by('-build__completed_on')
513
514 return render( request, template, context )
515
516
517def recipe(request, build_id, recipe_id):
518 template = "recipe.html"
519 if Recipe.objects.filter(pk=recipe_id).count() == 0 :
520 return redirect(builds)
521
522 object = Recipe.objects.filter(pk=recipe_id)[0]
523 layer_version = Layer_Version.objects.filter(pk=object.layer_version_id)[0]
524 layer = Layer.objects.filter(pk=layer_version.layer_id)[0]
525 tasks = Task.objects.filter(recipe_id = recipe_id, build_id = build_id).exclude(order__isnull=True).exclude(task_name__endswith='_setscene').exclude(outcome=Task.OUTCOME_NA)
526 packages = Package.objects.filter(recipe_id = recipe_id).filter(build_id = build_id).filter(size__gte=0)
527
528 context = {
529 'build' : Build.objects.filter(pk=build_id)[0],
530 'object' : object,
531 'layer_version' : layer_version,
532 'layer' : layer,
533 'tasks' : tasks,
534 'packages': packages,
535 }
536 return render(request, template, context)
537
538def target(request, build_id, target_id):
539 template = "target.html"
540 mandatory_parameters = { 'count': 25, 'page' : 1, 'orderby':'name:+'};
541 retval = _verify_parameters( request.GET, mandatory_parameters )
542 if retval:
543 return _redirect_parameters( 'target', request.GET, mandatory_parameters, build_id = build_id, target_id = target_id)
544 (filter_string, search_term, ordering_string) = _search_tuple(request, Package)
545
546 # FUTURE: get rid of nested sub-queries replacing with ManyToMany field
547 queryset = Package.objects.filter(size__gte=0, id__in=Target_Installed_Package.objects.filter(target_id=target_id).values('package_id'))
548 packages_sum = queryset.aggregate(Sum('installed_size'))
549 queryset = _get_queryset(Package, queryset, filter_string, search_term, ordering_string, 'name')
550 packages = _build_page_range(Paginator(queryset, request.GET.get('count', 25)),request.GET.get('page', 1))
551
552 # bring in package dependencies
553 for p in packages.object_list:
554 p.runtime_dependencies = p.package_dependencies_source.filter(target_id = target_id, dep_type=Package_Dependency.TYPE_TRDEPENDS)
555 p.reverse_runtime_dependencies = p.package_dependencies_target.filter(target_id = target_id, dep_type=Package_Dependency.TYPE_TRDEPENDS)
556
557 context = { 'build': Build.objects.filter(pk=build_id)[0],
558 'target': Target.objects.filter(pk=target_id)[0],
559 'objects': packages,
560 'packages_sum' : packages_sum['installed_size__sum'],
561 'object_search_display': "packages included",
562 'tablecols':[
563 {
564 'name':'Package',
565 'qhelp':'Packaged output resulting from building a recipe and included in this image',
566 'orderfield': _get_toggle_order(request, "name"),
567 'ordericon':_get_toggle_order_icon(request, "name"),
568 },
569 {
570 'name':'Package version',
571 'qhelp':'The package version and revision',
572 },
573 {
574 'name':'Size',
575 'qhelp':'The size of the package',
576 'orderfield': _get_toggle_order(request, "size", True),
577 'ordericon':_get_toggle_order_icon(request, "size"),
578 'clclass': 'package_size span2',
579 'hidden' : 0,
580 },
581 {
582 'name':'Size over total (%)',
583 'qhelp':'Proportion of the overall included package size represented by this package',
584 'clclass': 'size_over_total span2',
585 'hidden' : 1,
586 },
587 {
588 'name':'License',
589 'qhelp':'The license under which the package is distributed. Multiple license names separated by the pipe character indicates a choice between licenses. Multiple license names separated by the ampersand character indicates multiple licenses exist that cover different parts of the source',
590 'orderfield': _get_toggle_order(request, "license"),
591 'ordericon':_get_toggle_order_icon(request, "license"),
592 'clclass': 'license',
593 'hidden' : 1,
594 },
595 {
596 'name':'Dependencies',
597 'qhelp':"Package runtime dependencies (i.e. other packages)",
598 'clclass': 'depends',
599 'hidden' : 0,
600 },
601 {
602 'name':'Reverse dependencies',
603 'qhelp':'Package run-time reverse dependencies (i.e. other packages that depend on this package)',
604 'clclass': 'brought_in_by',
605 'hidden' : 0,
606 },
607 {
608 'name':'Recipe',
609 'qhelp':'The name of the recipe building the package',
610 'orderfield': _get_toggle_order(request, "recipe__name"),
611 'ordericon':_get_toggle_order_icon(request, "recipe__name"),
612 'clclass': 'recipe_name',
613 'hidden' : 0,
614 },
615 {
616 'name':'Recipe version',
617 'qhelp':'Version and revision of the recipe building the package',
618 'clclass': 'recipe_version',
619 'hidden' : 1,
620 },
621 {
622 'name':'Layer',
623 'qhelp':'The name of the layer providing the recipe that builds the package',
624 'orderfield': _get_toggle_order(request, "recipe__layer_version__layer__name"),
625 'ordericon':_get_toggle_order_icon(request, "recipe__layer_version__layer__name"),
626 'clclass': 'layer_name',
627 'hidden' : 1,
628 },
629 {
630 'name':'Layer branch',
631 'qhelp':'The Git branch of the layer providing the recipe that builds the package',
632 'orderfield': _get_toggle_order(request, "recipe__layer_version__branch"),
633 'ordericon':_get_toggle_order_icon(request, "recipe__layer_version__branch"),
634 'clclass': 'layer_branch',
635 'hidden' : 1,
636 },
637 {
638 'name':'Layer commit',
639 'qhelp':'The Git commit of the layer providing the recipe that builds the package',
640 'clclass': 'layer_commit',
641 'hidden' : 1,
642 },
643 {
644 'name':'Layer directory',
645 'qhelp':'Path to the layer providing the recipe that builds the package',
646 'orderfield': _get_toggle_order(request, "recipe__layer_version__layer__local_path"),
647 'ordericon':_get_toggle_order_icon(request, "recipe__layer_version__layer__local_path"),
648 'clclass': 'layer_directory',
649 'hidden' : 1,
650 },
651 ]
652 }
653
654 return render(request, template, context)
655
656from django.core.serializers.json import DjangoJSONEncoder
657from django.http import HttpResponse
658def dirinfo_ajax(request, build_id, target_id):
659 top = request.GET.get('start', '/')
660 return HttpResponse(_get_dir_entries(build_id, target_id, top))
661
662from django.utils.functional import Promise
663from django.utils.encoding import force_text
664class LazyEncoder(json.JSONEncoder):
665 def default(self, obj):
666 if isinstance(obj, Promise):
667 return force_text(obj)
668 return super(LazyEncoder, self).default(obj)
669
670from toastergui.templatetags.projecttags import filtered_filesizeformat
671from django import template
672import os
673def _get_dir_entries(build_id, target_id, start):
674 node_str = {
675 Target_File.ITYPE_REGULAR : '-',
676 Target_File.ITYPE_DIRECTORY : 'd',
677 Target_File.ITYPE_SYMLINK : 'l',
678 Target_File.ITYPE_SOCKET : 's',
679 Target_File.ITYPE_FIFO : 'p',
680 Target_File.ITYPE_CHARACTER : 'c',
681 Target_File.ITYPE_BLOCK : 'b',
682 }
683 response = []
684 objects = Target_File.objects.filter(target__exact=target_id, directory__path=start)
685 target_packages = Target_Installed_Package.objects.filter(target__exact=target_id).values_list('package_id', flat=True)
686 for o in objects:
687 # exclude root inode '/'
688 if o.path == '/':
689 continue
690 try:
691 entry = {}
692 entry['parent'] = start
693 entry['name'] = os.path.basename(o.path)
694 entry['fullpath'] = o.path
695
696 # set defaults, not all dentries have packages
697 entry['installed_package'] = None
698 entry['package_id'] = None
699 entry['package'] = None
700 entry['link_to'] = None
701 if o.inodetype == Target_File.ITYPE_DIRECTORY:
702 entry['isdir'] = 1
703 # is there content in directory
704 entry['childcount'] = Target_File.objects.filter(target__exact=target_id, directory__path=o.path).all().count()
705 else:
706 entry['isdir'] = 0
707
708 # resolve the file to get the package from the resolved file
709 resolved_id = o.sym_target_id
710 resolved_path = o.path
711 if target_packages.count():
712 while resolved_id != "" and resolved_id != None:
713 tf = Target_File.objects.get(pk=resolved_id)
714 resolved_path = tf.path
715 resolved_id = tf.sym_target_id
716
717 thisfile=Package_File.objects.all().filter(path__exact=resolved_path, package_id__in=target_packages)
718 if thisfile.count():
719 p = Package.objects.get(pk=thisfile[0].package_id)
720 entry['installed_package'] = p.installed_name
721 entry['package_id'] = str(p.id)
722 entry['package'] = p.name
723 # don't use resolved path from above, show immediate link-to
724 if o.sym_target_id != "" and o.sym_target_id != None:
725 entry['link_to'] = Target_File.objects.get(pk=o.sym_target_id).path
726 t = template.Template('{% load projecttags %} {{ size|filtered_filesizeformat }}')
727 c = template.Context({'size': o.size})
728 entry['size'] = str(t.render(c))
729 if entry['link_to'] != None:
730 entry['permission'] = node_str[o.inodetype] + o.permission
731 else:
732 entry['permission'] = node_str[o.inodetype] + o.permission
733 entry['owner'] = o.owner
734 entry['group'] = o.group
735 response.append(entry)
736
737 except:
738 pass
739
740 # sort by directories first, then by name
741 rsorted = sorted(response, key=lambda entry : entry['name'])
742 rsorted = sorted(rsorted, key=lambda entry : entry['isdir'], reverse=True)
743 return json.dumps(rsorted, cls=LazyEncoder)
744
745def dirinfo(request, build_id, target_id, file_path=None):
746 template = "dirinfo.html"
747 objects = _get_dir_entries(build_id, target_id, '/')
748 packages_sum = Package.objects.filter(id__in=Target_Installed_Package.objects.filter(target_id=target_id).values('package_id')).aggregate(Sum('installed_size'))
749 dir_list = None
750 if file_path != None:
751 """
752 Link from the included package detail file list page and is
753 requesting opening the dir info to a specific file path.
754 Provide the list of directories to expand and the full path to
755 highlight in the page.
756 """
757 # Aassume target's path separator matches host's, that is, os.sep
758 sep = os.sep
759 dir_list = []
760 head = file_path
761 while head != sep:
762 (head,tail) = os.path.split(head)
763 if head != sep:
764 dir_list.insert(0, head)
765
766 context = { 'build': Build.objects.filter(pk=build_id)[0],
767 'target': Target.objects.filter(pk=target_id)[0],
768 'packages_sum': packages_sum['installed_size__sum'],
769 'objects': objects,
770 'dir_list': dir_list,
771 'file_path': file_path,
772 }
773 return render(request, template, context)
774
775def _find_task_dep(task):
776 tp = []
777 for p in Task_Dependency.objects.filter(task=task):
778 if (p.depends_on.order > 0) and (p.depends_on.outcome != Task.OUTCOME_NA):
779 tp.append(p.depends_on);
780 return tp
781
782
783def _find_task_revdep(task):
784 tp = []
785 for p in Task_Dependency.objects.filter(depends_on=task):
786 if (p.task.order > 0) and (p.task.outcome != Task.OUTCOME_NA):
787 tp.append(p.task);
788 return tp
789
790def _find_task_provider(task):
791 task_revdeps = _find_task_revdep(task)
792 for tr in task_revdeps:
793 if tr.outcome != Task.OUTCOME_COVERED:
794 return tr
795 for tr in task_revdeps:
796 trc = _find_task_provider(tr)
797 if trc is not None:
798 return trc
799 return None
800
801def tasks_common(request, build_id, variant, task_anchor):
802# This class is shared between these pages
803#
804# Column tasks buildtime diskio cpuusage
805# --------- ------ ---------- ------- ---------
806# Cache def
807# CPU min -
808# Disk min -
809# Executed def def def def
810# Log
811# Order def +
812# Outcome def def def def
813# Recipe min min min min
814# Version
815# Task min min min min
816# Time min -
817#
818# 'min':on always, 'def':on by default, else hidden
819# '+' default column sort up, '-' default column sort down
820
821 anchor = request.GET.get('anchor', '')
822 if not anchor:
823 anchor=task_anchor
824
825 # default ordering depends on variant
826 if 'buildtime' == variant:
827 title_variant='Time'
828 object_search_display="time data"
829 filter_search_display="tasks"
830 mandatory_parameters = { 'count': 25, 'page' : 1, 'orderby':'elapsed_time:-'};
831 elif 'diskio' == variant:
832 title_variant='Disk I/O'
833 object_search_display="disk I/O data"
834 filter_search_display="tasks"
835 mandatory_parameters = { 'count': 25, 'page' : 1, 'orderby':'disk_io:-'};
836 elif 'cpuusage' == variant:
837 title_variant='CPU usage'
838 object_search_display="CPU usage data"
839 filter_search_display="tasks"
840 mandatory_parameters = { 'count': 25, 'page' : 1, 'orderby':'cpu_usage:-'};
841 else :
842 title_variant='Tasks'
843 object_search_display="tasks"
844 filter_search_display="tasks"
845 mandatory_parameters = { 'count': 25, 'page' : 1, 'orderby':'order:+'};
846
847 template = 'tasks.html'
848 retval = _verify_parameters( request.GET, mandatory_parameters )
849 if retval:
850 if task_anchor:
851 mandatory_parameters['anchor']=task_anchor
852 return _redirect_parameters( variant, request.GET, mandatory_parameters, build_id = build_id)
853 (filter_string, search_term, ordering_string) = _search_tuple(request, Task)
854 queryset_all = Task.objects.filter(build=build_id).exclude(order__isnull=True).exclude(outcome=Task.OUTCOME_NA)
855 queryset_with_search = _get_queryset(Task, queryset_all, None , search_term, ordering_string, 'order')
856 queryset = _get_queryset(Task, queryset_all, filter_string, search_term, ordering_string, 'order')
857
858 # compute the anchor's page
859 if anchor:
860 request.GET = request.GET.copy()
861 del request.GET['anchor']
862 i=0
863 a=int(anchor)
864 count_per_page=int(request.GET.get('count', 100))
865 for task in queryset.iterator():
866 if a == task.order:
867 new_page= (i / count_per_page ) + 1
868 request.GET.__setitem__('page', new_page)
869 mandatory_parameters['page']=new_page
870 return _redirect_parameters( variant, request.GET, mandatory_parameters, build_id = build_id)
871 i += 1
872
873 tasks = _build_page_range(Paginator(queryset, request.GET.get('count', 100)),request.GET.get('page', 1))
874
875 # define (and modify by variants) the 'tablecols' members
876 tc_order={
877 'name':'Order',
878 'qhelp':'The running sequence of each task in the build',
879 'clclass': 'order', 'hidden' : 1,
880 'orderfield':_get_toggle_order(request, "order"),
881 'ordericon':_get_toggle_order_icon(request, "order")}
882 if 'tasks' == variant: tc_order['hidden']='0'; del tc_order['clclass']
883 tc_recipe={
884 'name':'Recipe',
885 'qhelp':'The name of the recipe to which each task applies',
886 'orderfield': _get_toggle_order(request, "recipe__name"),
887 'ordericon':_get_toggle_order_icon(request, "recipe__name"),
888 }
889 tc_recipe_version={
890 'name':'Recipe version',
891 'qhelp':'The version of the recipe to which each task applies',
892 'clclass': 'recipe_version', 'hidden' : 1,
893 }
894 tc_task={
895 'name':'Task',
896 'qhelp':'The name of the task',
897 'orderfield': _get_toggle_order(request, "task_name"),
898 'ordericon':_get_toggle_order_icon(request, "task_name"),
899 }
900 tc_executed={
901 'name':'Executed',
902 'qhelp':"This value tells you if a task had to run (executed) in order to generate the task output, or if the output was provided by another task and therefore the task didn't need to run (not executed)",
903 'clclass': 'executed', 'hidden' : 0,
904 'orderfield': _get_toggle_order(request, "task_executed"),
905 'ordericon':_get_toggle_order_icon(request, "task_executed"),
906 'filter' : {
907 'class' : 'executed',
908 'label': 'Show:',
909 'options' : [
910 ('Executed Tasks', 'task_executed:1', queryset_with_search.filter(task_executed=1).count()),
911 ('Not Executed Tasks', 'task_executed:0', queryset_with_search.filter(task_executed=0).count()),
912 ]
913 }
914
915 }
916 tc_outcome={
917 'name':'Outcome',
918 'qhelp':"This column tells you if 'executed' tasks succeeded or failed. The column also tells you why 'not executed' tasks did not need to run",
919 'clclass': 'outcome', 'hidden' : 0,
920 'orderfield': _get_toggle_order(request, "outcome"),
921 'ordericon':_get_toggle_order_icon(request, "outcome"),
922 'filter' : {
923 'class' : 'outcome',
924 'label': 'Show:',
925 'options' : [
926 ('Succeeded Tasks', 'outcome:%d'%Task.OUTCOME_SUCCESS, queryset_with_search.filter(outcome=Task.OUTCOME_SUCCESS).count(), "'Succeeded' tasks are those that ran and completed during the build" ),
927 ('Failed Tasks', 'outcome:%d'%Task.OUTCOME_FAILED, queryset_with_search.filter(outcome=Task.OUTCOME_FAILED).count(), "'Failed' tasks are those that ran but did not complete during the build"),
928 ('Cached Tasks', 'outcome:%d'%Task.OUTCOME_CACHED, queryset_with_search.filter(outcome=Task.OUTCOME_CACHED).count(), 'Cached tasks restore output from the <code>sstate-cache</code> directory or mirrors'),
929 ('Prebuilt Tasks', 'outcome:%d'%Task.OUTCOME_PREBUILT, queryset_with_search.filter(outcome=Task.OUTCOME_PREBUILT).count(),'Prebuilt tasks didn\'t need to run because their output was reused from a previous build'),
930 ('Covered Tasks', 'outcome:%d'%Task.OUTCOME_COVERED, queryset_with_search.filter(outcome=Task.OUTCOME_COVERED).count(), 'Covered tasks didn\'t need to run because their output is provided by another task in this build'),
931 ('Empty Tasks', 'outcome:%d'%Task.OUTCOME_EMPTY, queryset_with_search.filter(outcome=Task.OUTCOME_NA).count(), 'Empty tasks have no executable content'),
932 ]
933 }
934
935 }
936 tc_log={
937 'name':'Log',
938 'qhelp':'Path to the task log file',
939 'orderfield': _get_toggle_order(request, "logfile"),
940 'ordericon':_get_toggle_order_icon(request, "logfile"),
941 'clclass': 'task_log', 'hidden' : 1,
942 }
943 tc_cache={
944 'name':'Cache attempt',
945 'qhelp':'This column tells you if a task tried to restore output from the <code>sstate-cache</code> directory or mirrors, and reports the result: Succeeded, Failed or File not in cache',
946 'clclass': 'cache_attempt', 'hidden' : 0,
947 'orderfield': _get_toggle_order(request, "sstate_result"),
948 'ordericon':_get_toggle_order_icon(request, "sstate_result"),
949 'filter' : {
950 'class' : 'cache_attempt',
951 'label': 'Show:',
952 'options' : [
953 ('Tasks with cache attempts', 'sstate_result__gt:%d'%Task.SSTATE_NA, queryset_with_search.filter(sstate_result__gt=Task.SSTATE_NA).count(), 'Show all tasks that tried to restore ouput from the <code>sstate-cache</code> directory or mirrors'),
954 ("Tasks with 'File not in cache' attempts", 'sstate_result:%d'%Task.SSTATE_MISS, queryset_with_search.filter(sstate_result=Task.SSTATE_MISS).count(), 'Show tasks that tried to restore output, but did not find it in the <code>sstate-cache</code> directory or mirrors'),
955 ("Tasks with 'Failed' cache attempts", 'sstate_result:%d'%Task.SSTATE_FAILED, queryset_with_search.filter(sstate_result=Task.SSTATE_FAILED).count(), 'Show tasks that found the required output in the <code>sstate-cache</code> directory or mirrors, but could not restore it'),
956 ("Tasks with 'Succeeded' cache attempts", 'sstate_result:%d'%Task.SSTATE_RESTORED, queryset_with_search.filter(sstate_result=Task.SSTATE_RESTORED).count(), 'Show tasks that successfully restored the required output from the <code>sstate-cache</code> directory or mirrors'),
957 ]
958 }
959
960 }
961 #if 'tasks' == variant: tc_cache['hidden']='0';
962 tc_time={
963 'name':'Time (secs)',
964 'qhelp':'How long it took the task to finish in seconds',
965 'orderfield': _get_toggle_order(request, "elapsed_time", True),
966 'ordericon':_get_toggle_order_icon(request, "elapsed_time"),
967 'clclass': 'time_taken', 'hidden' : 1,
968 }
969 if 'buildtime' == variant: tc_time['hidden']='0'; del tc_time['clclass']; tc_cache['hidden']='1';
970 tc_cpu={
971 'name':'CPU usage',
972 'qhelp':'The percentage of task CPU utilization',
973 'orderfield': _get_toggle_order(request, "cpu_usage", True),
974 'ordericon':_get_toggle_order_icon(request, "cpu_usage"),
975 'clclass': 'cpu_used', 'hidden' : 1,
976 }
977 if 'cpuusage' == variant: tc_cpu['hidden']='0'; del tc_cpu['clclass']; tc_cache['hidden']='1';
978 tc_diskio={
979 'name':'Disk I/O (ms)',
980 'qhelp':'Number of miliseconds the task spent doing disk input and output',
981 'orderfield': _get_toggle_order(request, "disk_io", True),
982 'ordericon':_get_toggle_order_icon(request, "disk_io"),
983 'clclass': 'disk_io', 'hidden' : 1,
984 }
985 if 'diskio' == variant: tc_diskio['hidden']='0'; del tc_diskio['clclass']; tc_cache['hidden']='1';
986
987
988 context = { 'objectname': variant,
989 'object_search_display': object_search_display,
990 'filter_search_display': filter_search_display,
991 'title': title_variant,
992 'build': Build.objects.filter(pk=build_id)[0],
993 'objects': tasks,
994 'search_term': search_term,
995 'total_count': queryset_with_search.count(),
996 'tablecols':[
997 tc_order,
998 tc_recipe,
999 tc_recipe_version,
1000 tc_task,
1001 tc_executed,
1002 tc_outcome,
1003 tc_cache,
1004 tc_time,
1005 tc_cpu,
1006 tc_diskio,
1007 tc_log,
1008 ]}
1009
1010 return render(request, template, context)
1011
1012def tasks(request, build_id):
1013 return tasks_common(request, build_id, 'tasks', '')
1014
1015def tasks_task(request, build_id, task_id):
1016 return tasks_common(request, build_id, 'tasks', task_id)
1017
1018def buildtime(request, build_id):
1019 return tasks_common(request, build_id, 'buildtime', '')
1020
1021def diskio(request, build_id):
1022 return tasks_common(request, build_id, 'diskio', '')
1023
1024def cpuusage(request, build_id):
1025 return tasks_common(request, build_id, 'cpuusage', '')
1026
1027
1028def recipes(request, build_id):
1029 template = 'recipes.html'
1030 mandatory_parameters = { 'count': 100, 'page' : 1, 'orderby':'name:+'};
1031 retval = _verify_parameters( request.GET, mandatory_parameters )
1032 if retval:
1033 return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id)
1034 (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe)
1035 queryset = Recipe.objects.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id))
1036 queryset = _get_queryset(Recipe, queryset, filter_string, search_term, ordering_string, 'name')
1037
1038 recipes = _build_page_range(Paginator(queryset, request.GET.get('count', 100)),request.GET.get('page', 1))
1039
1040 context = {
1041 'objectname': 'recipes',
1042 'build': Build.objects.filter(pk=build_id)[0],
1043 'objects': recipes,
1044 'tablecols':[
1045 {
1046 'name':'Recipe',
1047 'qhelp':'Information about a single piece of software, including where to download the source, configuration options, how to compile the source files and how to package the compiled output',
1048 'orderfield': _get_toggle_order(request, "name"),
1049 'ordericon':_get_toggle_order_icon(request, "name"),
1050 },
1051 {
1052 'name':'Recipe version',
1053 'qhelp':'The recipe version and revision',
1054 },
1055 {
1056 'name':'Dependencies',
1057 'qhelp':'Recipe build-time dependencies (i.e. other recipes)',
1058 'clclass': 'depends_on', 'hidden': 1,
1059 },
1060 {
1061 'name':'Reverse dependencies',
1062 'qhelp':'Recipe build-time reverse dependencies (i.e. the recipes that depend on this recipe)',
1063 'clclass': 'depends_by', 'hidden': 1,
1064 },
1065 {
1066 'name':'Recipe file',
1067 'qhelp':'Path to the recipe .bb file',
1068 'orderfield': _get_toggle_order(request, "file_path"),
1069 'ordericon':_get_toggle_order_icon(request, "file_path"),
1070 'clclass': 'recipe_file', 'hidden': 0,
1071 },
1072 {
1073 'name':'Section',
1074 'qhelp':'The section in which recipes should be categorized',
1075 'orderfield': _get_toggle_order(request, "section"),
1076 'ordericon':_get_toggle_order_icon(request, "section"),
1077 'clclass': 'recipe_section', 'hidden': 0,
1078 },
1079 {
1080 'name':'License',
1081 'qhelp':'The list of source licenses for the recipe. Multiple license names separated by the pipe character indicates a choice between licenses. Multiple license names separated by the ampersand character indicates multiple licenses exist that cover different parts of the source',
1082 'orderfield': _get_toggle_order(request, "license"),
1083 'ordericon':_get_toggle_order_icon(request, "license"),
1084 'clclass': 'recipe_license', 'hidden': 0,
1085 },
1086 {
1087 'name':'Layer',
1088 'qhelp':'The name of the layer providing the recipe',
1089 'orderfield': _get_toggle_order(request, "layer_version__layer__name"),
1090 'ordericon':_get_toggle_order_icon(request, "layer_version__layer__name"),
1091 'clclass': 'layer_version__layer__name', 'hidden': 0,
1092 },
1093 {
1094 'name':'Layer branch',
1095 'qhelp':'The Git branch of the layer providing the recipe',
1096 'orderfield': _get_toggle_order(request, "layer_version__branch"),
1097 'ordericon':_get_toggle_order_icon(request, "layer_version__branch"),
1098 'clclass': 'layer_version__branch', 'hidden': 1,
1099 },
1100 {
1101 'name':'Layer commit',
1102 'qhelp':'The Git commit of the layer providing the recipe',
1103 'clclass': 'layer_version__layer__commit', 'hidden': 1,
1104 },
1105 {
1106 'name':'Layer directory',
1107 'qhelp':'Path to the layer prodiving the recipe',
1108 'orderfield': _get_toggle_order(request, "layer_version__layer__local_path"),
1109 'ordericon':_get_toggle_order_icon(request, "layer_version__layer__local_path"),
1110 'clclass': 'layer_version__layer__local_path', 'hidden': 1,
1111 },
1112 ]
1113 }
1114
1115 return render(request, template, context)
1116
1117
1118def configuration(request, build_id):
1119 template = 'configuration.html'
1120
1121 variables = Variable.objects.filter(build=build_id)
1122 BB_VERSION=variables.filter(variable_name='BB_VERSION')[0].variable_value
1123 BUILD_SYS=variables.filter(variable_name='BUILD_SYS')[0].variable_value
1124 NATIVELSBSTRING=variables.filter(variable_name='NATIVELSBSTRING')[0].variable_value
1125 TARGET_SYS=variables.filter(variable_name='TARGET_SYS')[0].variable_value
1126 MACHINE=variables.filter(variable_name='MACHINE')[0].variable_value
1127 DISTRO=variables.filter(variable_name='DISTRO')[0].variable_value
1128 DISTRO_VERSION=variables.filter(variable_name='DISTRO_VERSION')[0].variable_value
1129 TUNE_FEATURES=variables.filter(variable_name='TUNE_FEATURES')[0].variable_value
1130 TARGET_FPU=variables.filter(variable_name='TARGET_FPU')[0].variable_value
1131
1132 targets = Target.objects.filter(build=build_id)
1133
1134 context = {
1135 'objectname': 'configuration',
1136 'object_search_display':'variables',
1137 'filter_search_display':'variables',
1138 'build': Build.objects.filter(pk=build_id)[0],
1139 'BB_VERSION':BB_VERSION,
1140 'BUILD_SYS':BUILD_SYS,
1141 'NATIVELSBSTRING':NATIVELSBSTRING,
1142 'TARGET_SYS':TARGET_SYS,
1143 'MACHINE':MACHINE,
1144 'DISTRO':DISTRO,
1145 'DISTRO_VERSION':DISTRO_VERSION,
1146 'TUNE_FEATURES':TUNE_FEATURES,
1147 'TARGET_FPU':TARGET_FPU,
1148 'targets':targets,
1149 }
1150 return render(request, template, context)
1151
1152
1153def configvars(request, build_id):
1154 template = 'configvars.html'
1155 mandatory_parameters = { 'count': 100, 'page' : 1, 'orderby':'variable_name:+', 'filter':'description__regex:.+'};
1156 retval = _verify_parameters( request.GET, mandatory_parameters )
1157 (filter_string, search_term, ordering_string) = _search_tuple(request, Variable)
1158 if retval:
1159 # if new search, clear the default filter
1160 if search_term and len(search_term):
1161 mandatory_parameters['filter']=''
1162 return _redirect_parameters( 'configvars', request.GET, mandatory_parameters, build_id = build_id)
1163
1164 queryset = Variable.objects.filter(build=build_id).exclude(variable_name__istartswith='B_').exclude(variable_name__istartswith='do_')
1165 queryset_with_search = _get_queryset(Variable, queryset, None, search_term, ordering_string, 'variable_name').exclude(variable_value='',vhistory__file_name__isnull=True)
1166 queryset = _get_queryset(Variable, queryset, filter_string, search_term, ordering_string, 'variable_name')
1167 # remove records where the value is empty AND there are no history files
1168 queryset = queryset.exclude(variable_value='',vhistory__file_name__isnull=True)
1169
1170 variables = _build_page_range(Paginator(queryset, request.GET.get('count', 50)), request.GET.get('page', 1))
1171
1172 # show all matching files (not just the last one)
1173 file_filter= search_term + ":"
1174 if filter_string.find('/conf/') > 0:
1175 file_filter += 'conf/(local|bblayers).conf'
1176 if filter_string.find('conf/machine/') > 0:
1177 file_filter += 'conf/machine/'
1178 if filter_string.find('conf/distro/') > 0:
1179 file_filter += 'conf/distro/'
1180 if filter_string.find('/bitbake.conf') > 0:
1181 file_filter += '/bitbake.conf'
1182 build_dir=re.sub("/tmp/log/.*","",Build.objects.filter(pk=build_id)[0].cooker_log_path)
1183
1184 context = {
1185 'objectname': 'configvars',
1186 'object_search_display':'BitBake variables',
1187 'filter_search_display':'variables',
1188 'file_filter': file_filter,
1189 'build': Build.objects.filter(pk=build_id)[0],
1190 'objects' : variables,
1191 'total_count':queryset_with_search.count(),
1192 'search_term':search_term,
1193 # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
1194 'tablecols' : [
1195 {'name': 'Variable',
1196 'qhelp': "BitBake is a generic task executor that considers a list of tasks with dependencies and handles metadata that consists of variables in a certain format that get passed to the tasks",
1197 'orderfield': _get_toggle_order(request, "variable_name"),
1198 'ordericon':_get_toggle_order_icon(request, "variable_name"),
1199 },
1200 {'name': 'Value',
1201 'qhelp': "The value assigned to the variable",
1202 'dclass': "span4",
1203 },
1204 {'name': 'Set in file',
1205 'qhelp': "The last configuration file that touched the variable value",
1206 'clclass': 'file', 'hidden' : 0,
1207 'filter' : {
1208 'class' : 'vhistory__file_name',
1209 'label': 'Show:',
1210 'options' : [
1211 ('Local configuration variables', 'vhistory__file_name__contains:'+build_dir+'/conf/',queryset_with_search.filter(vhistory__file_name__contains=build_dir+'/conf/').count(), 'Select this filter to see variables set by the <code>local.conf</code> and <code>bblayers.conf</code> configuration files inside the <code>/build/conf/</code> directory'),
1212 ('Machine configuration variables', 'vhistory__file_name__contains:conf/machine/',queryset_with_search.filter(vhistory__file_name__contains='conf/machine').count(), 'Select this filter to see variables set by the configuration file(s) inside your layers <code>/conf/machine/</code> directory'),
1213 ('Distro configuration variables', 'vhistory__file_name__contains:conf/distro/',queryset_with_search.filter(vhistory__file_name__contains='conf/distro').count(), 'Select this filter to see variables set by the configuration file(s) inside your layers <code>/conf/distro/</code> directory'),
1214 ('Layer configuration variables', 'vhistory__file_name__contains:conf/layer.conf',queryset_with_search.filter(vhistory__file_name__contains='conf/layer.conf').count(), 'Select this filter to see variables set by the <code>layer.conf</code> configuration file inside your layers'),
1215 ('bitbake.conf variables', 'vhistory__file_name__contains:/bitbake.conf',queryset_with_search.filter(vhistory__file_name__contains='/bitbake.conf').count(), 'Select this filter to see variables set by the <code>bitbake.conf</code> configuration file'),
1216 ]
1217 },
1218 },
1219 {'name': 'Description',
1220 'qhelp': "A brief explanation of the variable",
1221 'clclass': 'description', 'hidden' : 0,
1222 'dclass': "span4",
1223 'filter' : {
1224 'class' : 'description',
1225 'label': 'Show:',
1226 'options' : [
1227 ('Variables with description', 'description__regex:.+', queryset_with_search.filter(description__regex='.+').count(), 'We provide descriptions for the most common BitBake variables. The list of descriptions lives in <code>meta/conf/documentation.conf</code>'),
1228 ]
1229 },
1230 },
1231 ],
1232 }
1233
1234 return render(request, template, context)
1235
1236
1237def bpackage(request, build_id):
1238 template = 'bpackage.html'
1239 mandatory_parameters = { 'count': 100, 'page' : 1, 'orderby':'name:+'};
1240 retval = _verify_parameters( request.GET, mandatory_parameters )
1241 if retval:
1242 return _redirect_parameters( 'packages', request.GET, mandatory_parameters, build_id = build_id)
1243 (filter_string, search_term, ordering_string) = _search_tuple(request, Package)
1244 queryset = Package.objects.filter(build = build_id).filter(size__gte=0)
1245 queryset = _get_queryset(Package, queryset, filter_string, search_term, ordering_string, 'name')
1246
1247 packages = _build_page_range(Paginator(queryset, request.GET.get('count', 100)),request.GET.get('page', 1))
1248
1249 context = {
1250 'objectname': 'packages built',
1251 'build': Build.objects.filter(pk=build_id)[0],
1252 'objects' : packages,
1253 'tablecols':[
1254 {
1255 'name':'Package',
1256 'qhelp':'Packaged output resulting from building a recipe',
1257 'orderfield': _get_toggle_order(request, "name"),
1258 'ordericon':_get_toggle_order_icon(request, "name"),
1259 },
1260 {
1261 'name':'Package version',
1262 'qhelp':'The package version and revision',
1263 },
1264 {
1265 'name':'Size',
1266 'qhelp':'The size of the package',
1267 'orderfield': _get_toggle_order(request, "size", True),
1268 'ordericon':_get_toggle_order_icon(request, "size"),
1269 'clclass': 'size span2', 'hidden': 0,
1270 },
1271 {
1272 'name':'License',
1273 'qhelp':'The license under which the package is distributed. Multiple license names separated by the pipe character indicates a choice between licenses. Multiple license names separated by the ampersand character indicates multiple licenses exist that cover different parts of the source',
1274 'orderfield': _get_toggle_order(request, "license"),
1275 'ordericon':_get_toggle_order_icon(request, "license"),
1276 'clclass': 'license', 'hidden': 1,
1277 },
1278 {
1279 'name':'Recipe',
1280 'qhelp':'The name of the recipe building the package',
1281 'orderfield': _get_toggle_order(request, "recipe__name"),
1282 'ordericon':_get_toggle_order_icon(request, "recipe__name"),
1283 'clclass': 'recipe__name', 'hidden': 0,
1284 },
1285 {
1286 'name':'Recipe version',
1287 'qhelp':'Version and revision of the recipe building the package',
1288 'clclass': 'recipe__version', 'hidden': 1,
1289 },
1290 {
1291 'name':'Layer',
1292 'qhelp':'The name of the layer providing the recipe that builds the package',
1293 'orderfield': _get_toggle_order(request, "recipe__layer_version__layer__name"),
1294 'ordericon':_get_toggle_order_icon(request, "recipe__layer_version__layer__name"),
1295 'clclass': 'recipe__layer_version__layer__name', 'hidden': 1,
1296 },
1297 {
1298 'name':'Layer branch',
1299 'qhelp':'The Git branch of the layer providing the recipe that builds the package',
1300 'orderfield': _get_toggle_order(request, "recipe__layer_version__branch"),
1301 'ordericon':_get_toggle_order_icon(request, "recipe__layer_version__branch"),
1302 'clclass': 'recipe__layer_version__branch', 'hidden': 1,
1303 },
1304 {
1305 'name':'Layer commit',
1306 'qhelp':'The Git commit of the layer providing the recipe that builds the package',
1307 'clclass': 'recipe__layer_version__layer__commit', 'hidden': 1,
1308 },
1309 {
1310 'name':'Layer directory',
1311 'qhelp':'Path to the layer providing the recipe that builds the package',
1312 'orderfield': _get_toggle_order(request, "recipe__layer_version__layer__local_path"),
1313 'ordericon':_get_toggle_order_icon(request, "recipe__layer_version__layer__local_path"),
1314 'clclass': 'recipe__layer_version__layer__local_path', 'hidden': 1,
1315 },
1316 ]
1317 }
1318
1319 return render(request, template, context)
1320
1321def bfile(request, build_id, package_id):
1322 template = 'bfile.html'
1323 files = Package_File.objects.filter(package = package_id)
1324 context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : files}
1325 return render(request, template, context)
1326
1327def tpackage(request, build_id, target_id):
1328 template = 'package.html'
1329 packages = map(lambda x: x.package, list(Target_Installed_Package.objects.filter(target=target_id)))
1330 context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : packages}
1331 return render(request, template, context)
1332
1333def layer(request):
1334 template = 'layer.html'
1335 layer_info = Layer.objects.all()
1336
1337 for li in layer_info:
1338 li.versions = Layer_Version.objects.filter(layer = li)
1339 for liv in li.versions:
1340 liv.count = Recipe.objects.filter(layer_version__id = liv.id).count()
1341
1342 context = {'objects': layer_info}
1343
1344 return render(request, template, context)
1345
1346
1347def layer_versions_recipes(request, layerversion_id):
1348 template = 'recipe.html'
1349 recipes = Recipe.objects.filter(layer_version__id = layerversion_id)
1350
1351 context = {'objects': recipes,
1352 'layer_version' : Layer_Version.objects.filter( id = layerversion_id )[0]
1353 }
1354
1355 return render(request, template, context)
1356
1357# A set of dependency types valid for both included and built package views
1358OTHER_DEPENDS_BASE = [
1359 Package_Dependency.TYPE_RSUGGESTS,
1360 Package_Dependency.TYPE_RPROVIDES,
1361 Package_Dependency.TYPE_RREPLACES,
1362 Package_Dependency.TYPE_RCONFLICTS,
1363 ]
1364
1365# value for invalid row id
1366INVALID_KEY = -1
1367
1368"""
1369Given a package id, target_id retrieves two sets of this image and package's
1370dependencies. The return value is a dictionary consisting of two other
1371lists: a list of 'runtime' dependencies, that is, having RDEPENDS
1372values in source package's recipe, and a list of other dependencies, that is
1373the list of possible recipe variables as found in OTHER_DEPENDS_BASE plus
1374the RRECOMENDS or TRECOMENDS value.
1375The lists are built in the sort order specified for the package runtime
1376dependency views.
1377"""
1378def _get_package_dependencies(package_id, target_id = INVALID_KEY):
1379 runtime_deps = []
1380 other_deps = []
1381 other_depends_types = OTHER_DEPENDS_BASE
1382
1383 if target_id != INVALID_KEY :
1384 rdepends_type = Package_Dependency.TYPE_TRDEPENDS
1385 other_depends_types += [Package_Dependency.TYPE_TRECOMMENDS]
1386 else :
1387 rdepends_type = Package_Dependency.TYPE_RDEPENDS
1388 other_depends_types += [Package_Dependency.TYPE_RRECOMMENDS]
1389
1390 package = Package.objects.get(pk=package_id)
1391 if target_id != INVALID_KEY :
1392 alldeps = package.package_dependencies_source.filter(target_id__exact = target_id)
1393 else :
1394 alldeps = package.package_dependencies_source.all()
1395 for idep in alldeps:
1396 dep_package = Package.objects.get(pk=idep.depends_on_id)
1397 dep_entry = Package_Dependency.DEPENDS_DICT[idep.dep_type]
1398 if dep_package.version == '' :
1399 version = ''
1400 else :
1401 version = dep_package.version + "-" + dep_package.revision
1402 installed = False
1403 if target_id != INVALID_KEY :
1404 if Target_Installed_Package.objects.filter(target_id__exact = target_id, package_id__exact = dep_package.id).count() > 0:
1405 installed = True
1406 dep = {
1407 'name' : dep_package.name,
1408 'version' : version,
1409 'size' : dep_package.size,
1410 'dep_type' : idep.dep_type,
1411 'dep_type_display' : dep_entry[0].capitalize(),
1412 'dep_type_help' : dep_entry[1] % (dep_package.name, package.name),
1413 'depends_on_id' : dep_package.id,
1414 'installed' : installed,
1415 }
1416
1417 if target_id != INVALID_KEY:
1418 dep['alias'] = _get_package_alias(dep_package)
1419
1420 if idep.dep_type == rdepends_type :
1421 runtime_deps.append(dep)
1422 elif idep.dep_type in other_depends_types :
1423 other_deps.append(dep)
1424
1425 rdep_sorted = sorted(runtime_deps, key=lambda k: k['name'])
1426 odep_sorted = sorted(
1427 sorted(other_deps, key=lambda k: k['name']),
1428 key=lambda k: k['dep_type'])
1429 retvalues = {'runtime_deps' : rdep_sorted, 'other_deps' : odep_sorted}
1430 return retvalues
1431
1432# Return the count of packages dependent on package for this target_id image
1433def _get_package_reverse_dep_count(package, target_id):
1434 return package.package_dependencies_target.filter(target_id__exact=target_id, dep_type__exact = Package_Dependency.TYPE_TRDEPENDS).count()
1435
1436# Return the count of the packages that this package_id is dependent on.
1437# Use one of the two RDEPENDS types, either TRDEPENDS if the package was
1438# installed, or else RDEPENDS if only built.
1439def _get_package_dependency_count(package, target_id, is_installed):
1440 if is_installed :
1441 return package.package_dependencies_source.filter(target_id__exact = target_id,
1442 dep_type__exact = Package_Dependency.TYPE_TRDEPENDS).count()
1443 else :
1444 return package.package_dependencies_source.filter(dep_type__exact = Package_Dependency.TYPE_RDEPENDS).count()
1445
1446def _get_package_alias(package):
1447 alias = package.installed_name
1448 if alias != None and alias != '' and alias != package.name:
1449 return alias
1450 else:
1451 return ''
1452
1453def _get_fullpackagespec(package):
1454 r = package.name
1455 version_good = package.version != None and package.version != ''
1456 revision_good = package.revision != None and package.revision != ''
1457 if version_good or revision_good:
1458 r += '_'
1459 if version_good:
1460 r += package.version
1461 if revision_good:
1462 r += '-'
1463 if revision_good:
1464 r += package.revision
1465 return r
1466
1467def package_built_detail(request, build_id, package_id):
1468 template = "package_built_detail.html"
1469 if Build.objects.filter(pk=build_id).count() == 0 :
1470 return redirect(builds)
1471
1472 # follow convention for pagination w/ search although not used for this view
1473 queryset = Package_File.objects.filter(package_id__exact=package_id)
1474 mandatory_parameters = { 'count': 25, 'page' : 1, 'orderby':'path:+'};
1475 retval = _verify_parameters( request.GET, mandatory_parameters )
1476 if retval:
1477 return _redirect_parameters( 'package_built_detail', request.GET, mandatory_parameters, build_id = build_id, package_id = package_id)
1478
1479 (filter_string, search_term, ordering_string) = _search_tuple(request, Package_File)
1480 paths = _get_queryset(Package_File, queryset, filter_string, search_term, ordering_string, 'path')
1481
1482 package = Package.objects.filter(pk=package_id)[0]
1483 package.fullpackagespec = _get_fullpackagespec(package)
1484 context = {
1485 'build' : Build.objects.filter(pk=build_id)[0],
1486 'package' : package,
1487 'dependency_count' : _get_package_dependency_count(package, -1, False),
1488 'objects' : paths,
1489 'tablecols':[
1490 {
1491 'name':'File',
1492 'orderfield': _get_toggle_order(request, "path"),
1493 'ordericon':_get_toggle_order_icon(request, "path"),
1494 },
1495 {
1496 'name':'Size',
1497 'orderfield': _get_toggle_order(request, "size", True),
1498 'ordericon':_get_toggle_order_icon(request, "size"),
1499 'dclass': 'sizecol span2',
1500 },
1501 ]
1502 }
1503 if paths.all().count() < 2:
1504 context['disable_sort'] = True;
1505 return render(request, template, context)
1506
1507def package_built_dependencies(request, build_id, package_id):
1508 template = "package_built_dependencies.html"
1509 if Build.objects.filter(pk=build_id).count() == 0 :
1510 return redirect(builds)
1511
1512 package = Package.objects.filter(pk=package_id)[0]
1513 package.fullpackagespec = _get_fullpackagespec(package)
1514 dependencies = _get_package_dependencies(package_id)
1515 context = {
1516 'build' : Build.objects.filter(pk=build_id)[0],
1517 'package' : package,
1518 'runtime_deps' : dependencies['runtime_deps'],
1519 'other_deps' : dependencies['other_deps'],
1520 'dependency_count' : _get_package_dependency_count(package, -1, False)
1521 }
1522 return render(request, template, context)
1523
1524
1525def package_included_detail(request, build_id, target_id, package_id):
1526 template = "package_included_detail.html"
1527 if Build.objects.filter(pk=build_id).count() == 0 :
1528 return redirect(builds)
1529
1530
1531 # follow convention for pagination w/ search although not used for this view
1532 mandatory_parameters = { 'count': 25, 'page' : 1, 'orderby':'path:+'};
1533 retval = _verify_parameters( request.GET, mandatory_parameters )
1534 if retval:
1535 return _redirect_parameters( 'package_included_detail', request.GET, mandatory_parameters, build_id = build_id, target_id = target_id, package_id = package_id)
1536 (filter_string, search_term, ordering_string) = _search_tuple(request, Package_File)
1537
1538 queryset = Package_File.objects.filter(package_id__exact=package_id)
1539 paths = _get_queryset(Package_File, queryset, filter_string, search_term, ordering_string, 'path')
1540
1541 package = Package.objects.filter(pk=package_id)[0]
1542 package.fullpackagespec = _get_fullpackagespec(package)
1543 package.alias = _get_package_alias(package)
1544 target = Target.objects.filter(pk=target_id)[0]
1545 context = {
1546 'build' : Build.objects.filter(pk=build_id)[0],
1547 'target' : target,
1548 'package' : package,
1549 'reverse_count' : _get_package_reverse_dep_count(package, target_id),
1550 'dependency_count' : _get_package_dependency_count(package, target_id, True),
1551 'objects': paths,
1552 'tablecols':[
1553 {
1554 'name':'File',
1555 'orderfield': _get_toggle_order(request, "path"),
1556 'ordericon':_get_toggle_order_icon(request, "path"),
1557 },
1558 {
1559 'name':'Size',
1560 'orderfield': _get_toggle_order(request, "size", True),
1561 'ordericon':_get_toggle_order_icon(request, "size"),
1562 'dclass': 'sizecol span2',
1563 },
1564 ]
1565 }
1566 if paths.all().count() < 2:
1567 context['disable_sort'] = True;
1568 return render(request, template, context)
1569
1570def package_included_dependencies(request, build_id, target_id, package_id):
1571 template = "package_included_dependencies.html"
1572 if Build.objects.filter(pk=build_id).count() == 0 :
1573 return redirect(builds)
1574
1575 package = Package.objects.filter(pk=package_id)[0]
1576 package.fullpackagespec = _get_fullpackagespec(package)
1577 package.alias = _get_package_alias(package)
1578 target = Target.objects.filter(pk=target_id)[0]
1579
1580 dependencies = _get_package_dependencies(package_id, target_id)
1581 context = {
1582 'build' : Build.objects.filter(pk=build_id)[0],
1583 'package' : package,
1584 'target' : target,
1585 'runtime_deps' : dependencies['runtime_deps'],
1586 'other_deps' : dependencies['other_deps'],
1587 'reverse_count' : _get_package_reverse_dep_count(package, target_id),
1588 'dependency_count' : _get_package_dependency_count(package, target_id, True)
1589 }
1590 return render(request, template, context)
1591
1592def package_included_reverse_dependencies(request, build_id, target_id, package_id):
1593 template = "package_included_reverse_dependencies.html"
1594 if Build.objects.filter(pk=build_id).count() == 0 :
1595 return redirect(builds)
1596
1597 mandatory_parameters = { 'count': 25, 'page' : 1, 'orderby':'package__name:+'};
1598 retval = _verify_parameters( request.GET, mandatory_parameters )
1599 if retval:
1600 return _redirect_parameters( 'package_included_reverse_dependencies', request.GET, mandatory_parameters, build_id = build_id, target_id = target_id, package_id = package_id)
1601 (filter_string, search_term, ordering_string) = _search_tuple(request, Package_File)
1602
1603 queryset = Package_Dependency.objects.select_related('depends_on__name', 'depends_on__size').filter(depends_on=package_id, target_id=target_id, dep_type=Package_Dependency.TYPE_TRDEPENDS)
1604 objects = _get_queryset(Package_Dependency, queryset, filter_string, search_term, ordering_string, 'package__name')
1605
1606 package = Package.objects.filter(pk=package_id)[0]
1607 package.fullpackagespec = _get_fullpackagespec(package)
1608 package.alias = _get_package_alias(package)
1609 target = Target.objects.filter(pk=target_id)[0]
1610 for o in objects:
1611 if o.package.version != '':
1612 o.package.version += '-' + o.package.revision
1613 o.alias = _get_package_alias(o.package)
1614 context = {
1615 'build' : Build.objects.filter(pk=build_id)[0],
1616 'package' : package,
1617 'target' : target,
1618 'objects' : objects,
1619 'reverse_count' : _get_package_reverse_dep_count(package, target_id),
1620 'dependency_count' : _get_package_dependency_count(package, target_id, True),
1621 'tablecols':[
1622 {
1623 'name':'Package',
1624 'orderfield': _get_toggle_order(request, "package__name"),
1625 'ordericon': _get_toggle_order_icon(request, "package__name"),
1626 },
1627 {
1628 'name':'Version',
1629 },
1630 {
1631 'name':'Size',
1632 'orderfield': _get_toggle_order(request, "package__size", True),
1633 'ordericon': _get_toggle_order_icon(request, "package__size"),
1634 'dclass': 'sizecol span2',
1635 },
1636 ]
1637 }
1638 if objects.all().count() < 2:
1639 context['disable_sort'] = True;
1640 return render(request, template, context)
1641
1642def image_information_dir(request, build_id, target_id, packagefile_id):
1643 # stubbed for now
1644 return redirect(builds)
1645