xref: /OK3568_Linux_fs/yocto/poky/bitbake/lib/toaster/toastergui/widgets.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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