blob: b32abf40b379108211a149338d03ea56e50ccb4b [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05002# BitBake Toaster Implementation
3#
4# Copyright (C) 2015 Intel Corporation
5#
Brad Bishopc342db32019-05-15 21:57:59 -04006# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsc124f4f2015-09-15 14:41:29 -05007#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05008
9from django.views.generic import View, TemplateView
Andrew Geissler5082cc72023-09-11 08:41:39 -040010from django.utils.decorators import method_decorator
Patrick Williamsf1e5d692016-03-30 15:21:19 -050011from django.views.decorators.cache import cache_control
Patrick Williamsc124f4f2015-09-15 14:41:29 -050012from django.shortcuts import HttpResponse
Patrick Williamsc124f4f2015-09-15 14:41:29 -050013from django.core.cache import cache
14from django.core.paginator import Paginator, EmptyPage
15from django.db.models import Q
Patrick Williamsc0f7c042017-02-23 20:41:17 -060016from orm.models import Project, Build
Patrick Williamsc124f4f2015-09-15 14:41:29 -050017from django.template import Context, Template
Patrick Williamsc0f7c042017-02-23 20:41:17 -060018from django.template import VariableDoesNotExist
19from django.template import TemplateSyntaxError
Patrick Williamsc124f4f2015-09-15 14:41:29 -050020from django.core.serializers.json import DjangoJSONEncoder
21from django.core.exceptions import FieldError
Patrick Williamsc0f7c042017-02-23 20:41:17 -060022from django.utils import timezone
23from toastergui.templatetags.projecttags import sectohms, get_tasks
24from toastergui.templatetags.projecttags import json as template_json
25from django.http import JsonResponse
Andrew Geissler82c905d2020-04-13 13:39:40 -050026from django.urls import reverse
Patrick Williamsc124f4f2015-09-15 14:41:29 -050027
28import types
29import json
30import collections
Patrick Williamsc124f4f2015-09-15 14:41:29 -050031import re
Brad Bishopd7bf8c12018-02-25 22:55:05 -050032import os
Patrick Williamsc0f7c042017-02-23 20:41:17 -060033
Brad Bishop6e60e8b2018-02-01 10:27:11 -050034from toastergui.tablefilter import TableFilterMap
Andrew Geissler20137392023-10-12 04:59:14 -060035from toastermain.logs import log_view_mixin
Brad Bishop6e60e8b2018-02-01 10:27:11 -050036
Patrick Williamsc0f7c042017-02-23 20:41:17 -060037try:
38 from urllib import unquote_plus
39except ImportError:
40 from urllib.parse import unquote_plus
Patrick Williamsc124f4f2015-09-15 14:41:29 -050041
Patrick Williamsf1e5d692016-03-30 15:21:19 -050042import logging
43logger = logging.getLogger("toaster")
44
Patrick Williamsc0f7c042017-02-23 20:41:17 -060045
46class NoFieldOrDataName(Exception):
47 pass
48
49
Patrick Williamsc124f4f2015-09-15 14:41:29 -050050class ToasterTable(TemplateView):
51 def __init__(self, *args, **kwargs):
52 super(ToasterTable, self).__init__()
53 if 'template_name' in kwargs:
54 self.template_name = kwargs['template_name']
Patrick Williamsf1e5d692016-03-30 15:21:19 -050055 self.title = "Table"
Patrick Williamsc124f4f2015-09-15 14:41:29 -050056 self.queryset = None
57 self.columns = []
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050058
59 # map from field names to Filter instances
60 self.filter_map = TableFilterMap()
61
Patrick Williamsc124f4f2015-09-15 14:41:29 -050062 self.total_count = 0
63 self.static_context_extra = {}
Patrick Williamsc124f4f2015-09-15 14:41:29 -050064 self.empty_state = "Sorry - no data found"
65 self.default_orderby = ""
66
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050067 # prevent HTTP caching of table data
Andrew Geissler5082cc72023-09-11 08:41:39 -040068 @method_decorator(cache_control(must_revalidate=True,
69 max_age=0, no_store=True, no_cache=True))
Patrick Williamsf1e5d692016-03-30 15:21:19 -050070 def dispatch(self, *args, **kwargs):
71 return super(ToasterTable, self).dispatch(*args, **kwargs)
72
73 def get_context_data(self, **kwargs):
74 context = super(ToasterTable, self).get_context_data(**kwargs)
75 context['title'] = self.title
Patrick Williamsc0f7c042017-02-23 20:41:17 -060076 context['table_name'] = type(self).__name__.lower()
77 context['empty_state'] = self.empty_state
Patrick Williamsf1e5d692016-03-30 15:21:19 -050078
Brad Bishopd7bf8c12018-02-25 22:55:05 -050079 # global variables
80 context['project_enable'] = ('1' == os.environ.get('TOASTER_BUILDSERVER'))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080081 try:
82 context['project_specific'] = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC'))
83 except:
84 context['project_specific'] = ''
Brad Bishopd7bf8c12018-02-25 22:55:05 -050085
Patrick Williamsf1e5d692016-03-30 15:21:19 -050086 return context
87
Andrew Geissler20137392023-10-12 04:59:14 -060088 @log_view_mixin
Patrick Williamsc124f4f2015-09-15 14:41:29 -050089 def get(self, request, *args, **kwargs):
90 if request.GET.get('format', None) == 'json':
91
92 self.setup_queryset(*args, **kwargs)
93 # Put the project id into the context for the static_data_template
94 if 'pid' in kwargs:
95 self.static_context_extra['pid'] = kwargs['pid']
96
97 cmd = request.GET.get('cmd', None)
98 if cmd and 'filterinfo' in cmd:
99 data = self.get_filter_info(request, **kwargs)
100 else:
101 # If no cmd is specified we give you the table data
102 data = self.get_data(request, **kwargs)
103
104 return HttpResponse(data, content_type="application/json")
105
106 return super(ToasterTable, self).get(request, *args, **kwargs)
107
108 def get_filter_info(self, request, **kwargs):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500109 self.setup_filters(**kwargs)
110
111 search = request.GET.get("search", None)
112 if search:
113 self.apply_search(search)
114
115 name = request.GET.get("name", None)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500116 table_filter = self.filter_map.get_filter(name)
117 return json.dumps(table_filter.to_json(self.queryset),
118 indent=2,
119 cls=DjangoJSONEncoder)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500120
121 def setup_columns(self, *args, **kwargs):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600122 """ function to implement in the subclass which sets up
123 the columns """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500124 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600125
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500126 def setup_filters(self, *args, **kwargs):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600127 """ function to implement in the subclass which sets up the
128 filters """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500129 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600130
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500131 def setup_queryset(self, *args, **kwargs):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600132 """ function to implement in the subclass which sets up the
133 queryset"""
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500134 pass
135
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500136 def add_filter(self, table_filter):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500137 """Add a filter to the table.
138
139 Args:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500140 table_filter: Filter instance
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500141 """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500142 self.filter_map.add_filter(table_filter.name, table_filter)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500143
144 def add_column(self, title="", help_text="",
145 orderable=False, hideable=True, hidden=False,
146 field_name="", filter_name=None, static_data_name=None,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500147 static_data_template=None):
148 """Add a column to the table.
149
150 Args:
151 title (str): Title for the table header
152 help_text (str): Optional help text to describe the column
153 orderable (bool): Whether the column can be ordered.
154 We order on the field_name.
155 hideable (bool): Whether the user can hide the column
156 hidden (bool): Whether the column is default hidden
157 field_name (str or list): field(s) required for this column's data
158 static_data_name (str, optional): The column's main identifier
159 which will replace the field_name.
160 static_data_template(str, optional): The template to be rendered
161 as data
162 """
163
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600164 self.columns.append({'title': title,
165 'help_text': help_text,
166 'orderable': orderable,
167 'hideable': hideable,
168 'hidden': hidden,
169 'field_name': field_name,
170 'filter_name': filter_name,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500171 'static_data_name': static_data_name,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600172 'static_data_template': static_data_template})
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500173
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500174 def set_column_hidden(self, title, hidden):
175 """
176 Set the hidden state of the column to the value of hidden
177 """
178 for col in self.columns:
179 if col['title'] == title:
180 col['hidden'] = hidden
181 break
182
183 def set_column_hideable(self, title, hideable):
184 """
185 Set the hideable state of the column to the value of hideable
186 """
187 for col in self.columns:
188 if col['title'] == title:
189 col['hideable'] = hideable
190 break
191
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500192 def render_static_data(self, template, row):
193 """Utility function to render the static data template"""
194
195 context = {
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600196 'extra': self.static_context_extra,
197 'data': row,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500198 }
199
200 context = Context(context)
201 template = Template(template)
202
203 return template.render(context)
204
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500205 def apply_filter(self, filters, filter_value, **kwargs):
206 """
207 Apply a filter submitted in the querystring to the ToasterTable
208
209 filters: (str) in the format:
210 '<filter name>:<action name>'
211 filter_value: (str) parameters to pass to the named filter
212
213 <filter name> and <action name> are used to look up the correct filter
214 in the ToasterTable's filter map; the <action params> are set on
215 TableFilterAction* before its filter is applied and may modify the
216 queryset returned by the filter
217 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500218 self.setup_filters(**kwargs)
219
220 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500221 filter_name, action_name = filters.split(':')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600222 action_params = unquote_plus(filter_value)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500223 except ValueError:
224 return
225
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500226 if "all" in action_name:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500227 return
228
229 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500230 table_filter = self.filter_map.get_filter(filter_name)
231 action = table_filter.get_action(action_name)
232 action.set_filter_params(action_params)
233 self.queryset = action.filter(self.queryset)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500234 except KeyError:
235 # pass it to the user - programming error here
236 raise
237
238 def apply_orderby(self, orderby):
239 # Note that django will execute this when we try to retrieve the data
240 self.queryset = self.queryset.order_by(orderby)
241
242 def apply_search(self, search_term):
243 """Creates a query based on the model's search_allowed_fields"""
244
245 if not hasattr(self.queryset.model, 'search_allowed_fields'):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500246 raise Exception("Search fields aren't defined in the model %s"
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600247 % self.queryset.model)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500248
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600249 search_queries = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500250 for st in search_term.split(" "):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600251 queries = None
252 for field in self.queryset.model.search_allowed_fields:
253 query = Q(**{field + '__icontains': st})
254 if queries:
255 queries |= query
256 else:
257 queries = query
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500258
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600259 if search_queries:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500260 search_queries &= queries
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600261 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500262 search_queries = queries
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500263
264 self.queryset = self.queryset.filter(search_queries)
265
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500266 def get_data(self, request, **kwargs):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500267 """
268 Returns the data for the page requested with the specified
269 parameters applied
270
271 filters: filter and action name, e.g. "outcome:build_succeeded"
272 filter_value: value to pass to the named filter+action, e.g. "on"
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600273 (for a toggle filter) or "2015-12-11,2015-12-12"
274 (for a date range filter)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500275 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500276
277 page_num = request.GET.get("page", 1)
278 limit = request.GET.get("limit", 10)
279 search = request.GET.get("search", None)
280 filters = request.GET.get("filter", None)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500281 filter_value = request.GET.get("filter_value", "on")
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500282 orderby = request.GET.get("orderby", None)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500283 nocache = request.GET.get("nocache", None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500284
285 # Make a unique cache name
286 cache_name = self.__class__.__name__
287
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600288 for key, val in request.GET.items():
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500289 if key == 'nocache':
290 continue
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500291 cache_name = cache_name + str(key) + str(val)
292
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600293 for key, val in kwargs.items():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500294 cache_name = cache_name + str(key) + str(val)
295
296 # No special chars allowed in the cache name apart from dash
297 cache_name = re.sub(r'[^A-Za-z0-9-]', "", cache_name)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500298
299 if nocache:
300 cache.delete(cache_name)
301
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500302 data = cache.get(cache_name)
303
304 if data:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500305 logger.debug("Got cache data for table '%s'" % self.title)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500306 return data
307
308 self.setup_columns(**kwargs)
309
Andrew Geissler20137392023-10-12 04:59:14 -0600310 self.apply_orderby('pk')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500311 if search:
312 self.apply_search(search)
313 if filters:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500314 self.apply_filter(filters, filter_value, **kwargs)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500315 if orderby:
316 self.apply_orderby(orderby)
317
318 paginator = Paginator(self.queryset, limit)
319
320 try:
321 page = paginator.page(page_num)
322 except EmptyPage:
323 page = paginator.page(1)
324
325 data = {
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600326 'total': self.queryset.count(),
327 'default_orderby': self.default_orderby,
328 'columns': self.columns,
329 'rows': [],
330 'error': "ok",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500331 }
332
333 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600334 for model_obj in page.object_list:
335 # Use collection to maintain the order
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500336 required_data = collections.OrderedDict()
337
338 for col in self.columns:
339 field = col['field_name']
340 if not field:
341 field = col['static_data_name']
342 if not field:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600343 raise NoFieldOrDataName("Must supply a field_name or"
344 "static_data_name for column"
345 "%s.%s" %
346 (self.__class__.__name__, col)
347 )
348
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500349 # Check if we need to process some static data
350 if "static_data_name" in col and col['static_data_name']:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500351 # Overwrite the field_name with static_data_name
352 # so that this can be used as the html class name
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500353 col['field_name'] = col['static_data_name']
354
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600355 try:
356 # Render the template given
357 required_data[col['static_data_name']] = \
358 self.render_static_data(
359 col['static_data_template'], model_obj)
360 except (TemplateSyntaxError,
361 VariableDoesNotExist) as e:
362 logger.error("could not render template code"
363 "%s %s %s",
364 col['static_data_template'],
365 e, self.__class__.__name__)
366 required_data[col['static_data_name']] =\
367 '<!--error-->'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500368
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600369 else:
370 # Traverse to any foriegn key in the field
371 # e.g. recipe__layer_version__name
372 model_data = None
373
374 if "__" in field:
375 for subfield in field.split("__"):
376 if not model_data:
377 # The first iteration is always going to
378 # be on the actual model object instance.
379 # Subsequent ones are on the result of
380 # that. e.g. forieng key objects
381 model_data = getattr(model_obj,
382 subfield)
383 else:
384 model_data = getattr(model_data,
385 subfield)
386
387 else:
388 model_data = getattr(model_obj,
389 col['field_name'])
390
391 # We might have a model function as the field so
392 # call it to return the data needed
393 if isinstance(model_data, types.MethodType):
394 model_data = model_data()
395
396 required_data[col['field_name']] = model_data
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500397
398 data['rows'].append(required_data)
399
400 except FieldError:
401 # pass it to the user - programming-error here
402 raise
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600403
404 data = json.dumps(data, indent=2, cls=DjangoJSONEncoder)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500405 cache.set(cache_name, data, 60*30)
406
407 return data
408
409
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500410class ToasterTypeAhead(View):
411 """ A typeahead mechanism to support the front end typeahead widgets """
412 MAX_RESULTS = 6
413
414 class MissingFieldsException(Exception):
415 pass
416
417 def __init__(self, *args, **kwargs):
418 super(ToasterTypeAhead, self).__init__()
419
Andrew Geissler20137392023-10-12 04:59:14 -0600420 @log_view_mixin
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500421 def get(self, request, *args, **kwargs):
422 def response(data):
423 return HttpResponse(json.dumps(data,
424 indent=2,
425 cls=DjangoJSONEncoder),
426 content_type="application/json")
427
428 error = "ok"
429
430 search_term = request.GET.get("search", None)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600431 if search_term is None:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500432 # We got no search value so return empty reponse
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600433 return response({'error': error, 'results': []})
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500434
435 try:
436 prj = Project.objects.get(pk=kwargs['pid'])
437 except KeyError:
438 prj = None
439
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600440 results = self.apply_search(search_term,
441 prj,
442 request)[:ToasterTypeAhead.MAX_RESULTS]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500443
444 if len(results) > 0:
445 try:
446 self.validate_fields(results[0])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600447 except self.MissingFieldsException as e:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500448 error = e
449
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600450 data = {'results': results,
451 'error': error}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500452
453 return response(data)
454
455 def validate_fields(self, result):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600456 if 'name' in result is False or 'detail' in result is False:
457 raise self.MissingFieldsException(
458 "name and detail are required fields")
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500459
460 def apply_search(self, search_term, prj):
461 """ Override this function to implement search. Return an array of
462 dictionaries with a minium of a name and detail field"""
463 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600464
465
466class MostRecentBuildsView(View):
467 def _was_yesterday_or_earlier(self, completed_on):
468 now = timezone.now()
469 delta = now - completed_on
470
471 if delta.days >= 1:
472 return True
473
474 return False
475
Andrew Geissler20137392023-10-12 04:59:14 -0600476 @log_view_mixin
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600477 def get(self, request, *args, **kwargs):
478 """
479 Returns a list of builds in JSON format.
480 """
481 project = None
482
483 project_id = request.GET.get('project_id', None)
484 if project_id:
485 try:
486 project = Project.objects.get(pk=project_id)
487 except:
488 # if project lookup fails, assume no project
489 pass
490
491 recent_build_objs = Build.get_recent(project)
492 recent_builds = []
493
494 for build_obj in recent_build_objs:
495 dashboard_url = reverse('builddashboard', args=(build_obj.pk,))
496 buildtime_url = reverse('buildtime', args=(build_obj.pk,))
497 rebuild_url = \
498 reverse('xhr_buildrequest', args=(build_obj.project.pk,))
499 cancel_url = \
500 reverse('xhr_buildrequest', args=(build_obj.project.pk,))
501
502 build = {}
503 build['id'] = build_obj.pk
504 build['dashboard_url'] = dashboard_url
505
506 buildrequest_id = None
507 if hasattr(build_obj, 'buildrequest'):
508 buildrequest_id = build_obj.buildrequest.pk
509 build['buildrequest_id'] = buildrequest_id
510
Brad Bishop5dd7cbb2018-09-05 22:26:40 -0700511 if build_obj.recipes_to_parse > 0:
512 build['recipes_parsed_percentage'] = \
513 int((build_obj.recipes_parsed /
514 build_obj.recipes_to_parse) * 100)
515 else:
516 build['recipes_parsed_percentage'] = 0
517 if build_obj.repos_to_clone > 0:
518 build['repos_cloned_percentage'] = \
519 int((build_obj.repos_cloned /
520 build_obj.repos_to_clone) * 100)
521 else:
522 build['repos_cloned_percentage'] = 0
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500523
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800524 build['progress_item'] = build_obj.progress_item
525
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600526 tasks_complete_percentage = 0
527 if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED):
528 tasks_complete_percentage = 100
529 elif build_obj.outcome == Build.IN_PROGRESS:
530 tasks_complete_percentage = build_obj.completeper()
531 build['tasks_complete_percentage'] = tasks_complete_percentage
532
533 build['state'] = build_obj.get_state()
534
535 build['errors'] = build_obj.errors.count()
536 build['dashboard_errors_url'] = dashboard_url + '#errors'
537
538 build['warnings'] = build_obj.warnings.count()
539 build['dashboard_warnings_url'] = dashboard_url + '#warnings'
540
541 build['buildtime'] = sectohms(build_obj.timespent_seconds)
542 build['buildtime_url'] = buildtime_url
543
544 build['rebuild_url'] = rebuild_url
545 build['cancel_url'] = cancel_url
546
547 build['is_default_project_build'] = build_obj.project.is_default
548
549 build['build_targets_json'] = \
550 template_json(get_tasks(build_obj.target_set.all()))
551
552 # convert completed_on time to user's timezone
553 completed_on = timezone.localtime(build_obj.completed_on)
554
555 completed_on_template = '%H:%M'
556 if self._was_yesterday_or_earlier(completed_on):
557 completed_on_template = '%d/%m/%Y ' + completed_on_template
558 build['completed_on'] = completed_on.strftime(
559 completed_on_template)
560
561 targets = []
562 target_objs = build_obj.get_sorted_target_list()
563 for target_obj in target_objs:
564 if target_obj.task:
565 targets.append(target_obj.target + ':' + target_obj.task)
566 else:
567 targets.append(target_obj.target)
568 build['targets'] = ' '.join(targets)
569
570 # abbreviated form of the full target list
571 abbreviated_targets = ''
572 num_targets = len(targets)
573 if num_targets > 0:
574 abbreviated_targets = targets[0]
575 if num_targets > 1:
576 abbreviated_targets += (' +%s' % (num_targets - 1))
577 build['targets_abbreviated'] = abbreviated_targets
578
579 recent_builds.append(build)
580
581 return JsonResponse(recent_builds, safe=False)