1*4882a593Smuzhiyun# 2*4882a593Smuzhiyun# BitBake Toaster Implementation 3*4882a593Smuzhiyun# 4*4882a593Smuzhiyun# Copyright (C) 2015 Intel Corporation 5*4882a593Smuzhiyun# 6*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 7*4882a593Smuzhiyun# 8*4882a593Smuzhiyun 9*4882a593Smuzhiyunfrom django.views.generic import View, TemplateView 10*4882a593Smuzhiyunfrom django.views.decorators.cache import cache_control 11*4882a593Smuzhiyunfrom django.shortcuts import HttpResponse 12*4882a593Smuzhiyunfrom django.core.cache import cache 13*4882a593Smuzhiyunfrom django.core.paginator import Paginator, EmptyPage 14*4882a593Smuzhiyunfrom django.db.models import Q 15*4882a593Smuzhiyunfrom orm.models import Project, Build 16*4882a593Smuzhiyunfrom django.template import Context, Template 17*4882a593Smuzhiyunfrom django.template import VariableDoesNotExist 18*4882a593Smuzhiyunfrom django.template import TemplateSyntaxError 19*4882a593Smuzhiyunfrom django.core.serializers.json import DjangoJSONEncoder 20*4882a593Smuzhiyunfrom django.core.exceptions import FieldError 21*4882a593Smuzhiyunfrom django.utils import timezone 22*4882a593Smuzhiyunfrom toastergui.templatetags.projecttags import sectohms, get_tasks 23*4882a593Smuzhiyunfrom toastergui.templatetags.projecttags import json as template_json 24*4882a593Smuzhiyunfrom django.http import JsonResponse 25*4882a593Smuzhiyunfrom django.urls import reverse 26*4882a593Smuzhiyun 27*4882a593Smuzhiyunimport types 28*4882a593Smuzhiyunimport json 29*4882a593Smuzhiyunimport collections 30*4882a593Smuzhiyunimport re 31*4882a593Smuzhiyunimport os 32*4882a593Smuzhiyun 33*4882a593Smuzhiyunfrom toastergui.tablefilter import TableFilterMap 34*4882a593Smuzhiyun 35*4882a593Smuzhiyuntry: 36*4882a593Smuzhiyun from urllib import unquote_plus 37*4882a593Smuzhiyunexcept ImportError: 38*4882a593Smuzhiyun from urllib.parse import unquote_plus 39*4882a593Smuzhiyun 40*4882a593Smuzhiyunimport logging 41*4882a593Smuzhiyunlogger = logging.getLogger("toaster") 42*4882a593Smuzhiyun 43*4882a593Smuzhiyun 44*4882a593Smuzhiyunclass NoFieldOrDataName(Exception): 45*4882a593Smuzhiyun pass 46*4882a593Smuzhiyun 47*4882a593Smuzhiyun 48*4882a593Smuzhiyunclass ToasterTable(TemplateView): 49*4882a593Smuzhiyun def __init__(self, *args, **kwargs): 50*4882a593Smuzhiyun super(ToasterTable, self).__init__() 51*4882a593Smuzhiyun if 'template_name' in kwargs: 52*4882a593Smuzhiyun self.template_name = kwargs['template_name'] 53*4882a593Smuzhiyun self.title = "Table" 54*4882a593Smuzhiyun self.queryset = None 55*4882a593Smuzhiyun self.columns = [] 56*4882a593Smuzhiyun 57*4882a593Smuzhiyun # map from field names to Filter instances 58*4882a593Smuzhiyun self.filter_map = TableFilterMap() 59*4882a593Smuzhiyun 60*4882a593Smuzhiyun self.total_count = 0 61*4882a593Smuzhiyun self.static_context_extra = {} 62*4882a593Smuzhiyun self.empty_state = "Sorry - no data found" 63*4882a593Smuzhiyun self.default_orderby = "" 64*4882a593Smuzhiyun 65*4882a593Smuzhiyun # prevent HTTP caching of table data 66*4882a593Smuzhiyun @cache_control(must_revalidate=True, 67*4882a593Smuzhiyun max_age=0, no_store=True, no_cache=True) 68*4882a593Smuzhiyun def dispatch(self, *args, **kwargs): 69*4882a593Smuzhiyun return super(ToasterTable, self).dispatch(*args, **kwargs) 70*4882a593Smuzhiyun 71*4882a593Smuzhiyun def get_context_data(self, **kwargs): 72*4882a593Smuzhiyun context = super(ToasterTable, self).get_context_data(**kwargs) 73*4882a593Smuzhiyun context['title'] = self.title 74*4882a593Smuzhiyun context['table_name'] = type(self).__name__.lower() 75*4882a593Smuzhiyun context['empty_state'] = self.empty_state 76*4882a593Smuzhiyun 77*4882a593Smuzhiyun # global variables 78*4882a593Smuzhiyun context['project_enable'] = ('1' == os.environ.get('TOASTER_BUILDSERVER')) 79*4882a593Smuzhiyun try: 80*4882a593Smuzhiyun context['project_specific'] = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC')) 81*4882a593Smuzhiyun except: 82*4882a593Smuzhiyun context['project_specific'] = '' 83*4882a593Smuzhiyun 84*4882a593Smuzhiyun return context 85*4882a593Smuzhiyun 86*4882a593Smuzhiyun def get(self, request, *args, **kwargs): 87*4882a593Smuzhiyun if request.GET.get('format', None) == 'json': 88*4882a593Smuzhiyun 89*4882a593Smuzhiyun self.setup_queryset(*args, **kwargs) 90*4882a593Smuzhiyun # Put the project id into the context for the static_data_template 91*4882a593Smuzhiyun if 'pid' in kwargs: 92*4882a593Smuzhiyun self.static_context_extra['pid'] = kwargs['pid'] 93*4882a593Smuzhiyun 94*4882a593Smuzhiyun cmd = request.GET.get('cmd', None) 95*4882a593Smuzhiyun if cmd and 'filterinfo' in cmd: 96*4882a593Smuzhiyun data = self.get_filter_info(request, **kwargs) 97*4882a593Smuzhiyun else: 98*4882a593Smuzhiyun # If no cmd is specified we give you the table data 99*4882a593Smuzhiyun data = self.get_data(request, **kwargs) 100*4882a593Smuzhiyun 101*4882a593Smuzhiyun return HttpResponse(data, content_type="application/json") 102*4882a593Smuzhiyun 103*4882a593Smuzhiyun return super(ToasterTable, self).get(request, *args, **kwargs) 104*4882a593Smuzhiyun 105*4882a593Smuzhiyun def get_filter_info(self, request, **kwargs): 106*4882a593Smuzhiyun self.setup_filters(**kwargs) 107*4882a593Smuzhiyun 108*4882a593Smuzhiyun search = request.GET.get("search", None) 109*4882a593Smuzhiyun if search: 110*4882a593Smuzhiyun self.apply_search(search) 111*4882a593Smuzhiyun 112*4882a593Smuzhiyun name = request.GET.get("name", None) 113*4882a593Smuzhiyun table_filter = self.filter_map.get_filter(name) 114*4882a593Smuzhiyun return json.dumps(table_filter.to_json(self.queryset), 115*4882a593Smuzhiyun indent=2, 116*4882a593Smuzhiyun cls=DjangoJSONEncoder) 117*4882a593Smuzhiyun 118*4882a593Smuzhiyun def setup_columns(self, *args, **kwargs): 119*4882a593Smuzhiyun """ function to implement in the subclass which sets up 120*4882a593Smuzhiyun the columns """ 121*4882a593Smuzhiyun pass 122*4882a593Smuzhiyun 123*4882a593Smuzhiyun def setup_filters(self, *args, **kwargs): 124*4882a593Smuzhiyun """ function to implement in the subclass which sets up the 125*4882a593Smuzhiyun filters """ 126*4882a593Smuzhiyun pass 127*4882a593Smuzhiyun 128*4882a593Smuzhiyun def setup_queryset(self, *args, **kwargs): 129*4882a593Smuzhiyun """ function to implement in the subclass which sets up the 130*4882a593Smuzhiyun queryset""" 131*4882a593Smuzhiyun pass 132*4882a593Smuzhiyun 133*4882a593Smuzhiyun def add_filter(self, table_filter): 134*4882a593Smuzhiyun """Add a filter to the table. 135*4882a593Smuzhiyun 136*4882a593Smuzhiyun Args: 137*4882a593Smuzhiyun table_filter: Filter instance 138*4882a593Smuzhiyun """ 139*4882a593Smuzhiyun self.filter_map.add_filter(table_filter.name, table_filter) 140*4882a593Smuzhiyun 141*4882a593Smuzhiyun def add_column(self, title="", help_text="", 142*4882a593Smuzhiyun orderable=False, hideable=True, hidden=False, 143*4882a593Smuzhiyun field_name="", filter_name=None, static_data_name=None, 144*4882a593Smuzhiyun static_data_template=None): 145*4882a593Smuzhiyun """Add a column to the table. 146*4882a593Smuzhiyun 147*4882a593Smuzhiyun Args: 148*4882a593Smuzhiyun title (str): Title for the table header 149*4882a593Smuzhiyun help_text (str): Optional help text to describe the column 150*4882a593Smuzhiyun orderable (bool): Whether the column can be ordered. 151*4882a593Smuzhiyun We order on the field_name. 152*4882a593Smuzhiyun hideable (bool): Whether the user can hide the column 153*4882a593Smuzhiyun hidden (bool): Whether the column is default hidden 154*4882a593Smuzhiyun field_name (str or list): field(s) required for this column's data 155*4882a593Smuzhiyun static_data_name (str, optional): The column's main identifier 156*4882a593Smuzhiyun which will replace the field_name. 157*4882a593Smuzhiyun static_data_template(str, optional): The template to be rendered 158*4882a593Smuzhiyun as data 159*4882a593Smuzhiyun """ 160*4882a593Smuzhiyun 161*4882a593Smuzhiyun self.columns.append({'title': title, 162*4882a593Smuzhiyun 'help_text': help_text, 163*4882a593Smuzhiyun 'orderable': orderable, 164*4882a593Smuzhiyun 'hideable': hideable, 165*4882a593Smuzhiyun 'hidden': hidden, 166*4882a593Smuzhiyun 'field_name': field_name, 167*4882a593Smuzhiyun 'filter_name': filter_name, 168*4882a593Smuzhiyun 'static_data_name': static_data_name, 169*4882a593Smuzhiyun 'static_data_template': static_data_template}) 170*4882a593Smuzhiyun 171*4882a593Smuzhiyun def set_column_hidden(self, title, hidden): 172*4882a593Smuzhiyun """ 173*4882a593Smuzhiyun Set the hidden state of the column to the value of hidden 174*4882a593Smuzhiyun """ 175*4882a593Smuzhiyun for col in self.columns: 176*4882a593Smuzhiyun if col['title'] == title: 177*4882a593Smuzhiyun col['hidden'] = hidden 178*4882a593Smuzhiyun break 179*4882a593Smuzhiyun 180*4882a593Smuzhiyun def set_column_hideable(self, title, hideable): 181*4882a593Smuzhiyun """ 182*4882a593Smuzhiyun Set the hideable state of the column to the value of hideable 183*4882a593Smuzhiyun """ 184*4882a593Smuzhiyun for col in self.columns: 185*4882a593Smuzhiyun if col['title'] == title: 186*4882a593Smuzhiyun col['hideable'] = hideable 187*4882a593Smuzhiyun break 188*4882a593Smuzhiyun 189*4882a593Smuzhiyun def render_static_data(self, template, row): 190*4882a593Smuzhiyun """Utility function to render the static data template""" 191*4882a593Smuzhiyun 192*4882a593Smuzhiyun context = { 193*4882a593Smuzhiyun 'extra': self.static_context_extra, 194*4882a593Smuzhiyun 'data': row, 195*4882a593Smuzhiyun } 196*4882a593Smuzhiyun 197*4882a593Smuzhiyun context = Context(context) 198*4882a593Smuzhiyun template = Template(template) 199*4882a593Smuzhiyun 200*4882a593Smuzhiyun return template.render(context) 201*4882a593Smuzhiyun 202*4882a593Smuzhiyun def apply_filter(self, filters, filter_value, **kwargs): 203*4882a593Smuzhiyun """ 204*4882a593Smuzhiyun Apply a filter submitted in the querystring to the ToasterTable 205*4882a593Smuzhiyun 206*4882a593Smuzhiyun filters: (str) in the format: 207*4882a593Smuzhiyun '<filter name>:<action name>' 208*4882a593Smuzhiyun filter_value: (str) parameters to pass to the named filter 209*4882a593Smuzhiyun 210*4882a593Smuzhiyun <filter name> and <action name> are used to look up the correct filter 211*4882a593Smuzhiyun in the ToasterTable's filter map; the <action params> are set on 212*4882a593Smuzhiyun TableFilterAction* before its filter is applied and may modify the 213*4882a593Smuzhiyun queryset returned by the filter 214*4882a593Smuzhiyun """ 215*4882a593Smuzhiyun self.setup_filters(**kwargs) 216*4882a593Smuzhiyun 217*4882a593Smuzhiyun try: 218*4882a593Smuzhiyun filter_name, action_name = filters.split(':') 219*4882a593Smuzhiyun action_params = unquote_plus(filter_value) 220*4882a593Smuzhiyun except ValueError: 221*4882a593Smuzhiyun return 222*4882a593Smuzhiyun 223*4882a593Smuzhiyun if "all" in action_name: 224*4882a593Smuzhiyun return 225*4882a593Smuzhiyun 226*4882a593Smuzhiyun try: 227*4882a593Smuzhiyun table_filter = self.filter_map.get_filter(filter_name) 228*4882a593Smuzhiyun action = table_filter.get_action(action_name) 229*4882a593Smuzhiyun action.set_filter_params(action_params) 230*4882a593Smuzhiyun self.queryset = action.filter(self.queryset) 231*4882a593Smuzhiyun except KeyError: 232*4882a593Smuzhiyun # pass it to the user - programming error here 233*4882a593Smuzhiyun raise 234*4882a593Smuzhiyun 235*4882a593Smuzhiyun def apply_orderby(self, orderby): 236*4882a593Smuzhiyun # Note that django will execute this when we try to retrieve the data 237*4882a593Smuzhiyun self.queryset = self.queryset.order_by(orderby) 238*4882a593Smuzhiyun 239*4882a593Smuzhiyun def apply_search(self, search_term): 240*4882a593Smuzhiyun """Creates a query based on the model's search_allowed_fields""" 241*4882a593Smuzhiyun 242*4882a593Smuzhiyun if not hasattr(self.queryset.model, 'search_allowed_fields'): 243*4882a593Smuzhiyun raise Exception("Search fields aren't defined in the model %s" 244*4882a593Smuzhiyun % self.queryset.model) 245*4882a593Smuzhiyun 246*4882a593Smuzhiyun search_queries = None 247*4882a593Smuzhiyun for st in search_term.split(" "): 248*4882a593Smuzhiyun queries = None 249*4882a593Smuzhiyun for field in self.queryset.model.search_allowed_fields: 250*4882a593Smuzhiyun query = Q(**{field + '__icontains': st}) 251*4882a593Smuzhiyun if queries: 252*4882a593Smuzhiyun queries |= query 253*4882a593Smuzhiyun else: 254*4882a593Smuzhiyun queries = query 255*4882a593Smuzhiyun 256*4882a593Smuzhiyun if search_queries: 257*4882a593Smuzhiyun search_queries &= queries 258*4882a593Smuzhiyun else: 259*4882a593Smuzhiyun search_queries = queries 260*4882a593Smuzhiyun 261*4882a593Smuzhiyun self.queryset = self.queryset.filter(search_queries) 262*4882a593Smuzhiyun 263*4882a593Smuzhiyun def get_data(self, request, **kwargs): 264*4882a593Smuzhiyun """ 265*4882a593Smuzhiyun Returns the data for the page requested with the specified 266*4882a593Smuzhiyun parameters applied 267*4882a593Smuzhiyun 268*4882a593Smuzhiyun filters: filter and action name, e.g. "outcome:build_succeeded" 269*4882a593Smuzhiyun filter_value: value to pass to the named filter+action, e.g. "on" 270*4882a593Smuzhiyun (for a toggle filter) or "2015-12-11,2015-12-12" 271*4882a593Smuzhiyun (for a date range filter) 272*4882a593Smuzhiyun """ 273*4882a593Smuzhiyun 274*4882a593Smuzhiyun page_num = request.GET.get("page", 1) 275*4882a593Smuzhiyun limit = request.GET.get("limit", 10) 276*4882a593Smuzhiyun search = request.GET.get("search", None) 277*4882a593Smuzhiyun filters = request.GET.get("filter", None) 278*4882a593Smuzhiyun filter_value = request.GET.get("filter_value", "on") 279*4882a593Smuzhiyun orderby = request.GET.get("orderby", None) 280*4882a593Smuzhiyun nocache = request.GET.get("nocache", None) 281*4882a593Smuzhiyun 282*4882a593Smuzhiyun # Make a unique cache name 283*4882a593Smuzhiyun cache_name = self.__class__.__name__ 284*4882a593Smuzhiyun 285*4882a593Smuzhiyun for key, val in request.GET.items(): 286*4882a593Smuzhiyun if key == 'nocache': 287*4882a593Smuzhiyun continue 288*4882a593Smuzhiyun cache_name = cache_name + str(key) + str(val) 289*4882a593Smuzhiyun 290*4882a593Smuzhiyun for key, val in kwargs.items(): 291*4882a593Smuzhiyun cache_name = cache_name + str(key) + str(val) 292*4882a593Smuzhiyun 293*4882a593Smuzhiyun # No special chars allowed in the cache name apart from dash 294*4882a593Smuzhiyun cache_name = re.sub(r'[^A-Za-z0-9-]', "", cache_name) 295*4882a593Smuzhiyun 296*4882a593Smuzhiyun if nocache: 297*4882a593Smuzhiyun cache.delete(cache_name) 298*4882a593Smuzhiyun 299*4882a593Smuzhiyun data = cache.get(cache_name) 300*4882a593Smuzhiyun 301*4882a593Smuzhiyun if data: 302*4882a593Smuzhiyun logger.debug("Got cache data for table '%s'" % self.title) 303*4882a593Smuzhiyun return data 304*4882a593Smuzhiyun 305*4882a593Smuzhiyun self.setup_columns(**kwargs) 306*4882a593Smuzhiyun 307*4882a593Smuzhiyun if search: 308*4882a593Smuzhiyun self.apply_search(search) 309*4882a593Smuzhiyun if filters: 310*4882a593Smuzhiyun self.apply_filter(filters, filter_value, **kwargs) 311*4882a593Smuzhiyun if orderby: 312*4882a593Smuzhiyun self.apply_orderby(orderby) 313*4882a593Smuzhiyun 314*4882a593Smuzhiyun paginator = Paginator(self.queryset, limit) 315*4882a593Smuzhiyun 316*4882a593Smuzhiyun try: 317*4882a593Smuzhiyun page = paginator.page(page_num) 318*4882a593Smuzhiyun except EmptyPage: 319*4882a593Smuzhiyun page = paginator.page(1) 320*4882a593Smuzhiyun 321*4882a593Smuzhiyun data = { 322*4882a593Smuzhiyun 'total': self.queryset.count(), 323*4882a593Smuzhiyun 'default_orderby': self.default_orderby, 324*4882a593Smuzhiyun 'columns': self.columns, 325*4882a593Smuzhiyun 'rows': [], 326*4882a593Smuzhiyun 'error': "ok", 327*4882a593Smuzhiyun } 328*4882a593Smuzhiyun 329*4882a593Smuzhiyun try: 330*4882a593Smuzhiyun for model_obj in page.object_list: 331*4882a593Smuzhiyun # Use collection to maintain the order 332*4882a593Smuzhiyun required_data = collections.OrderedDict() 333*4882a593Smuzhiyun 334*4882a593Smuzhiyun for col in self.columns: 335*4882a593Smuzhiyun field = col['field_name'] 336*4882a593Smuzhiyun if not field: 337*4882a593Smuzhiyun field = col['static_data_name'] 338*4882a593Smuzhiyun if not field: 339*4882a593Smuzhiyun raise NoFieldOrDataName("Must supply a field_name or" 340*4882a593Smuzhiyun "static_data_name for column" 341*4882a593Smuzhiyun "%s.%s" % 342*4882a593Smuzhiyun (self.__class__.__name__, col) 343*4882a593Smuzhiyun ) 344*4882a593Smuzhiyun 345*4882a593Smuzhiyun # Check if we need to process some static data 346*4882a593Smuzhiyun if "static_data_name" in col and col['static_data_name']: 347*4882a593Smuzhiyun # Overwrite the field_name with static_data_name 348*4882a593Smuzhiyun # so that this can be used as the html class name 349*4882a593Smuzhiyun col['field_name'] = col['static_data_name'] 350*4882a593Smuzhiyun 351*4882a593Smuzhiyun try: 352*4882a593Smuzhiyun # Render the template given 353*4882a593Smuzhiyun required_data[col['static_data_name']] = \ 354*4882a593Smuzhiyun self.render_static_data( 355*4882a593Smuzhiyun col['static_data_template'], model_obj) 356*4882a593Smuzhiyun except (TemplateSyntaxError, 357*4882a593Smuzhiyun VariableDoesNotExist) as e: 358*4882a593Smuzhiyun logger.error("could not render template code" 359*4882a593Smuzhiyun "%s %s %s", 360*4882a593Smuzhiyun col['static_data_template'], 361*4882a593Smuzhiyun e, self.__class__.__name__) 362*4882a593Smuzhiyun required_data[col['static_data_name']] =\ 363*4882a593Smuzhiyun '<!--error-->' 364*4882a593Smuzhiyun 365*4882a593Smuzhiyun else: 366*4882a593Smuzhiyun # Traverse to any foriegn key in the field 367*4882a593Smuzhiyun # e.g. recipe__layer_version__name 368*4882a593Smuzhiyun model_data = None 369*4882a593Smuzhiyun 370*4882a593Smuzhiyun if "__" in field: 371*4882a593Smuzhiyun for subfield in field.split("__"): 372*4882a593Smuzhiyun if not model_data: 373*4882a593Smuzhiyun # The first iteration is always going to 374*4882a593Smuzhiyun # be on the actual model object instance. 375*4882a593Smuzhiyun # Subsequent ones are on the result of 376*4882a593Smuzhiyun # that. e.g. forieng key objects 377*4882a593Smuzhiyun model_data = getattr(model_obj, 378*4882a593Smuzhiyun subfield) 379*4882a593Smuzhiyun else: 380*4882a593Smuzhiyun model_data = getattr(model_data, 381*4882a593Smuzhiyun subfield) 382*4882a593Smuzhiyun 383*4882a593Smuzhiyun else: 384*4882a593Smuzhiyun model_data = getattr(model_obj, 385*4882a593Smuzhiyun col['field_name']) 386*4882a593Smuzhiyun 387*4882a593Smuzhiyun # We might have a model function as the field so 388*4882a593Smuzhiyun # call it to return the data needed 389*4882a593Smuzhiyun if isinstance(model_data, types.MethodType): 390*4882a593Smuzhiyun model_data = model_data() 391*4882a593Smuzhiyun 392*4882a593Smuzhiyun required_data[col['field_name']] = model_data 393*4882a593Smuzhiyun 394*4882a593Smuzhiyun data['rows'].append(required_data) 395*4882a593Smuzhiyun 396*4882a593Smuzhiyun except FieldError: 397*4882a593Smuzhiyun # pass it to the user - programming-error here 398*4882a593Smuzhiyun raise 399*4882a593Smuzhiyun 400*4882a593Smuzhiyun data = json.dumps(data, indent=2, cls=DjangoJSONEncoder) 401*4882a593Smuzhiyun cache.set(cache_name, data, 60*30) 402*4882a593Smuzhiyun 403*4882a593Smuzhiyun return data 404*4882a593Smuzhiyun 405*4882a593Smuzhiyun 406*4882a593Smuzhiyunclass ToasterTypeAhead(View): 407*4882a593Smuzhiyun """ A typeahead mechanism to support the front end typeahead widgets """ 408*4882a593Smuzhiyun MAX_RESULTS = 6 409*4882a593Smuzhiyun 410*4882a593Smuzhiyun class MissingFieldsException(Exception): 411*4882a593Smuzhiyun pass 412*4882a593Smuzhiyun 413*4882a593Smuzhiyun def __init__(self, *args, **kwargs): 414*4882a593Smuzhiyun super(ToasterTypeAhead, self).__init__() 415*4882a593Smuzhiyun 416*4882a593Smuzhiyun def get(self, request, *args, **kwargs): 417*4882a593Smuzhiyun def response(data): 418*4882a593Smuzhiyun return HttpResponse(json.dumps(data, 419*4882a593Smuzhiyun indent=2, 420*4882a593Smuzhiyun cls=DjangoJSONEncoder), 421*4882a593Smuzhiyun content_type="application/json") 422*4882a593Smuzhiyun 423*4882a593Smuzhiyun error = "ok" 424*4882a593Smuzhiyun 425*4882a593Smuzhiyun search_term = request.GET.get("search", None) 426*4882a593Smuzhiyun if search_term is None: 427*4882a593Smuzhiyun # We got no search value so return empty reponse 428*4882a593Smuzhiyun return response({'error': error, 'results': []}) 429*4882a593Smuzhiyun 430*4882a593Smuzhiyun try: 431*4882a593Smuzhiyun prj = Project.objects.get(pk=kwargs['pid']) 432*4882a593Smuzhiyun except KeyError: 433*4882a593Smuzhiyun prj = None 434*4882a593Smuzhiyun 435*4882a593Smuzhiyun results = self.apply_search(search_term, 436*4882a593Smuzhiyun prj, 437*4882a593Smuzhiyun request)[:ToasterTypeAhead.MAX_RESULTS] 438*4882a593Smuzhiyun 439*4882a593Smuzhiyun if len(results) > 0: 440*4882a593Smuzhiyun try: 441*4882a593Smuzhiyun self.validate_fields(results[0]) 442*4882a593Smuzhiyun except self.MissingFieldsException as e: 443*4882a593Smuzhiyun error = e 444*4882a593Smuzhiyun 445*4882a593Smuzhiyun data = {'results': results, 446*4882a593Smuzhiyun 'error': error} 447*4882a593Smuzhiyun 448*4882a593Smuzhiyun return response(data) 449*4882a593Smuzhiyun 450*4882a593Smuzhiyun def validate_fields(self, result): 451*4882a593Smuzhiyun if 'name' in result is False or 'detail' in result is False: 452*4882a593Smuzhiyun raise self.MissingFieldsException( 453*4882a593Smuzhiyun "name and detail are required fields") 454*4882a593Smuzhiyun 455*4882a593Smuzhiyun def apply_search(self, search_term, prj): 456*4882a593Smuzhiyun """ Override this function to implement search. Return an array of 457*4882a593Smuzhiyun dictionaries with a minium of a name and detail field""" 458*4882a593Smuzhiyun pass 459*4882a593Smuzhiyun 460*4882a593Smuzhiyun 461*4882a593Smuzhiyunclass MostRecentBuildsView(View): 462*4882a593Smuzhiyun def _was_yesterday_or_earlier(self, completed_on): 463*4882a593Smuzhiyun now = timezone.now() 464*4882a593Smuzhiyun delta = now - completed_on 465*4882a593Smuzhiyun 466*4882a593Smuzhiyun if delta.days >= 1: 467*4882a593Smuzhiyun return True 468*4882a593Smuzhiyun 469*4882a593Smuzhiyun return False 470*4882a593Smuzhiyun 471*4882a593Smuzhiyun def get(self, request, *args, **kwargs): 472*4882a593Smuzhiyun """ 473*4882a593Smuzhiyun Returns a list of builds in JSON format. 474*4882a593Smuzhiyun """ 475*4882a593Smuzhiyun project = None 476*4882a593Smuzhiyun 477*4882a593Smuzhiyun project_id = request.GET.get('project_id', None) 478*4882a593Smuzhiyun if project_id: 479*4882a593Smuzhiyun try: 480*4882a593Smuzhiyun project = Project.objects.get(pk=project_id) 481*4882a593Smuzhiyun except: 482*4882a593Smuzhiyun # if project lookup fails, assume no project 483*4882a593Smuzhiyun pass 484*4882a593Smuzhiyun 485*4882a593Smuzhiyun recent_build_objs = Build.get_recent(project) 486*4882a593Smuzhiyun recent_builds = [] 487*4882a593Smuzhiyun 488*4882a593Smuzhiyun for build_obj in recent_build_objs: 489*4882a593Smuzhiyun dashboard_url = reverse('builddashboard', args=(build_obj.pk,)) 490*4882a593Smuzhiyun buildtime_url = reverse('buildtime', args=(build_obj.pk,)) 491*4882a593Smuzhiyun rebuild_url = \ 492*4882a593Smuzhiyun reverse('xhr_buildrequest', args=(build_obj.project.pk,)) 493*4882a593Smuzhiyun cancel_url = \ 494*4882a593Smuzhiyun reverse('xhr_buildrequest', args=(build_obj.project.pk,)) 495*4882a593Smuzhiyun 496*4882a593Smuzhiyun build = {} 497*4882a593Smuzhiyun build['id'] = build_obj.pk 498*4882a593Smuzhiyun build['dashboard_url'] = dashboard_url 499*4882a593Smuzhiyun 500*4882a593Smuzhiyun buildrequest_id = None 501*4882a593Smuzhiyun if hasattr(build_obj, 'buildrequest'): 502*4882a593Smuzhiyun buildrequest_id = build_obj.buildrequest.pk 503*4882a593Smuzhiyun build['buildrequest_id'] = buildrequest_id 504*4882a593Smuzhiyun 505*4882a593Smuzhiyun if build_obj.recipes_to_parse > 0: 506*4882a593Smuzhiyun build['recipes_parsed_percentage'] = \ 507*4882a593Smuzhiyun int((build_obj.recipes_parsed / 508*4882a593Smuzhiyun build_obj.recipes_to_parse) * 100) 509*4882a593Smuzhiyun else: 510*4882a593Smuzhiyun build['recipes_parsed_percentage'] = 0 511*4882a593Smuzhiyun if build_obj.repos_to_clone > 0: 512*4882a593Smuzhiyun build['repos_cloned_percentage'] = \ 513*4882a593Smuzhiyun int((build_obj.repos_cloned / 514*4882a593Smuzhiyun build_obj.repos_to_clone) * 100) 515*4882a593Smuzhiyun else: 516*4882a593Smuzhiyun build['repos_cloned_percentage'] = 0 517*4882a593Smuzhiyun 518*4882a593Smuzhiyun build['progress_item'] = build_obj.progress_item 519*4882a593Smuzhiyun 520*4882a593Smuzhiyun tasks_complete_percentage = 0 521*4882a593Smuzhiyun if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED): 522*4882a593Smuzhiyun tasks_complete_percentage = 100 523*4882a593Smuzhiyun elif build_obj.outcome == Build.IN_PROGRESS: 524*4882a593Smuzhiyun tasks_complete_percentage = build_obj.completeper() 525*4882a593Smuzhiyun build['tasks_complete_percentage'] = tasks_complete_percentage 526*4882a593Smuzhiyun 527*4882a593Smuzhiyun build['state'] = build_obj.get_state() 528*4882a593Smuzhiyun 529*4882a593Smuzhiyun build['errors'] = build_obj.errors.count() 530*4882a593Smuzhiyun build['dashboard_errors_url'] = dashboard_url + '#errors' 531*4882a593Smuzhiyun 532*4882a593Smuzhiyun build['warnings'] = build_obj.warnings.count() 533*4882a593Smuzhiyun build['dashboard_warnings_url'] = dashboard_url + '#warnings' 534*4882a593Smuzhiyun 535*4882a593Smuzhiyun build['buildtime'] = sectohms(build_obj.timespent_seconds) 536*4882a593Smuzhiyun build['buildtime_url'] = buildtime_url 537*4882a593Smuzhiyun 538*4882a593Smuzhiyun build['rebuild_url'] = rebuild_url 539*4882a593Smuzhiyun build['cancel_url'] = cancel_url 540*4882a593Smuzhiyun 541*4882a593Smuzhiyun build['is_default_project_build'] = build_obj.project.is_default 542*4882a593Smuzhiyun 543*4882a593Smuzhiyun build['build_targets_json'] = \ 544*4882a593Smuzhiyun template_json(get_tasks(build_obj.target_set.all())) 545*4882a593Smuzhiyun 546*4882a593Smuzhiyun # convert completed_on time to user's timezone 547*4882a593Smuzhiyun completed_on = timezone.localtime(build_obj.completed_on) 548*4882a593Smuzhiyun 549*4882a593Smuzhiyun completed_on_template = '%H:%M' 550*4882a593Smuzhiyun if self._was_yesterday_or_earlier(completed_on): 551*4882a593Smuzhiyun completed_on_template = '%d/%m/%Y ' + completed_on_template 552*4882a593Smuzhiyun build['completed_on'] = completed_on.strftime( 553*4882a593Smuzhiyun completed_on_template) 554*4882a593Smuzhiyun 555*4882a593Smuzhiyun targets = [] 556*4882a593Smuzhiyun target_objs = build_obj.get_sorted_target_list() 557*4882a593Smuzhiyun for target_obj in target_objs: 558*4882a593Smuzhiyun if target_obj.task: 559*4882a593Smuzhiyun targets.append(target_obj.target + ':' + target_obj.task) 560*4882a593Smuzhiyun else: 561*4882a593Smuzhiyun targets.append(target_obj.target) 562*4882a593Smuzhiyun build['targets'] = ' '.join(targets) 563*4882a593Smuzhiyun 564*4882a593Smuzhiyun # abbreviated form of the full target list 565*4882a593Smuzhiyun abbreviated_targets = '' 566*4882a593Smuzhiyun num_targets = len(targets) 567*4882a593Smuzhiyun if num_targets > 0: 568*4882a593Smuzhiyun abbreviated_targets = targets[0] 569*4882a593Smuzhiyun if num_targets > 1: 570*4882a593Smuzhiyun abbreviated_targets += (' +%s' % (num_targets - 1)) 571*4882a593Smuzhiyun build['targets_abbreviated'] = abbreviated_targets 572*4882a593Smuzhiyun 573*4882a593Smuzhiyun recent_builds.append(build) 574*4882a593Smuzhiyun 575*4882a593Smuzhiyun return JsonResponse(recent_builds, safe=False) 576