Squashed 'yocto-poky/' content from commit ea562de

git-subtree-dir: yocto-poky
git-subtree-split: ea562de57590c966cd5a75fda8defecd397e6436
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
new file mode 100644
index 0000000..eb2914d
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -0,0 +1,411 @@
+#
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2015        Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.views.generic import View, TemplateView
+from django.shortcuts import HttpResponse
+from django.http import HttpResponseBadRequest
+from django.core import serializers
+from django.core.cache import cache
+from django.core.paginator import Paginator, EmptyPage
+from django.db.models import Q
+from orm.models import Project, ProjectLayer, Layer_Version
+from django.template import Context, Template
+from django.core.serializers.json import DjangoJSONEncoder
+from django.core.exceptions import FieldError
+from django.conf.urls import url, patterns
+
+import types
+import json
+import collections
+import operator
+import re
+
+from toastergui.views import objtojson
+
+class ToasterTable(TemplateView):
+    def __init__(self, *args, **kwargs):
+        super(ToasterTable, self).__init__()
+        if 'template_name' in kwargs:
+            self.template_name = kwargs['template_name']
+        self.title = None
+        self.queryset = None
+        self.columns = []
+        self.filters = {}
+        self.total_count = 0
+        self.static_context_extra = {}
+        self.filter_actions = {}
+        self.empty_state = "Sorry - no data found"
+        self.default_orderby = ""
+
+        # add the "id" column, undisplayable, by default
+        self.add_column(title="Id",
+                        displayable=False,
+                        orderable=True,
+                        field_name="id")
+
+
+    def get(self, request, *args, **kwargs):
+        if request.GET.get('format', None) == 'json':
+
+            self.setup_queryset(*args, **kwargs)
+            # Put the project id into the context for the static_data_template
+            if 'pid' in kwargs:
+                self.static_context_extra['pid'] = kwargs['pid']
+
+            cmd = request.GET.get('cmd', None)
+            if cmd and 'filterinfo' in cmd:
+                data = self.get_filter_info(request, **kwargs)
+            else:
+                # If no cmd is specified we give you the table data
+                data = self.get_data(request, **kwargs)
+
+            return HttpResponse(data, content_type="application/json")
+
+        return super(ToasterTable, self).get(request, *args, **kwargs)
+
+    def get_filter_info(self, request, **kwargs):
+        data = None
+
+        self.setup_filters(**kwargs)
+
+        search = request.GET.get("search", None)
+        if search:
+            self.apply_search(search)
+
+        name = request.GET.get("name", None)
+        if name is None:
+            data = json.dumps(self.filters,
+                              indent=2,
+                              cls=DjangoJSONEncoder)
+        else:
+            for actions in self.filters[name]['filter_actions']:
+                actions['count'] = self.filter_actions[actions['name']](count_only=True)
+
+            # Add the "All" items filter action
+            self.filters[name]['filter_actions'].insert(0, {
+                'name' : 'all',
+                'title' : 'All',
+                'count' : self.queryset.count(),
+            })
+
+            data = json.dumps(self.filters[name],
+                              indent=2,
+                              cls=DjangoJSONEncoder)
+
+            return data
+
+    def setup_columns(self, *args, **kwargs):
+        """ function to implement in the subclass which sets up the columns """
+        pass
+    def setup_filters(self, *args, **kwargs):
+        """ function to implement in the subclass which sets up the filters """
+        pass
+    def setup_queryset(self, *args, **kwargs):
+        """ function to implement in the subclass which sets up the queryset"""
+        pass
+
+    def add_filter(self, name, title, filter_actions):
+        """Add a filter to the table.
+
+        Args:
+            name (str): Unique identifier of the filter.
+            title (str): Title of the filter.
+            filter_actions: Actions for all the filters.
+        """
+        self.filters[name] = {
+          'title' : title,
+          'filter_actions' : filter_actions,
+        }
+
+    def make_filter_action(self, name, title, action_function):
+        """ Utility to make a filter_action """
+
+        action = {
+          'title' : title,
+          'name' : name,
+        }
+
+        self.filter_actions[name] = action_function
+
+        return action
+
+    def add_column(self, title="", help_text="",
+                   orderable=False, hideable=True, hidden=False,
+                   field_name="", filter_name=None, static_data_name=None,
+                   displayable=True, computation=None,
+                   static_data_template=None):
+        """Add a column to the table.
+
+        Args:
+            title (str): Title for the table header
+            help_text (str): Optional help text to describe the column
+            orderable (bool): Whether the column can be ordered.
+                We order on the field_name.
+            hideable (bool): Whether the user can hide the column
+            hidden (bool): Whether the column is default hidden
+            field_name (str or list): field(s) required for this column's data
+            static_data_name (str, optional): The column's main identifier
+                which will replace the field_name.
+            static_data_template(str, optional): The template to be rendered
+                as data
+        """
+
+        self.columns.append({'title' : title,
+                             'help_text' : help_text,
+                             'orderable' : orderable,
+                             'hideable' : hideable,
+                             'hidden' : hidden,
+                             'field_name' : field_name,
+                             'filter_name' : filter_name,
+                             'static_data_name': static_data_name,
+                             'static_data_template': static_data_template,
+                             'displayable': displayable,
+                             'computation': computation,
+                            })
+
+    def render_static_data(self, template, row):
+        """Utility function to render the static data template"""
+
+        context = {
+          'extra' : self.static_context_extra,
+          'data' : row,
+        }
+
+        context = Context(context)
+        template = Template(template)
+
+        return template.render(context)
+
+    def apply_filter(self, filters, **kwargs):
+        self.setup_filters(**kwargs)
+
+        try:
+            filter_name, filter_action = filters.split(':')
+        except ValueError:
+            return
+
+        if "all" in filter_action:
+            return
+
+        try:
+            self.filter_actions[filter_action]()
+        except KeyError:
+            # pass it to the user - programming error here
+            raise
+
+    def apply_orderby(self, orderby):
+        # Note that django will execute this when we try to retrieve the data
+        self.queryset = self.queryset.order_by(orderby)
+
+    def apply_search(self, search_term):
+        """Creates a query based on the model's search_allowed_fields"""
+
+        if not hasattr(self.queryset.model, 'search_allowed_fields'):
+            raise Exception("Err Search fields aren't defined in the model")
+
+        search_queries = []
+        for st in search_term.split(" "):
+            q_map = [Q(**{field + '__icontains': st})
+                     for field in self.queryset.model.search_allowed_fields]
+
+            search_queries.append(reduce(operator.or_, q_map))
+
+        search_queries = reduce(operator.and_, search_queries)
+
+        self.queryset = self.queryset.filter(search_queries)
+
+
+    def get_data(self, request, **kwargs):
+        """Returns the data for the page requested with the specified
+        parameters applied"""
+
+        page_num = request.GET.get("page", 1)
+        limit = request.GET.get("limit", 10)
+        search = request.GET.get("search", None)
+        filters = request.GET.get("filter", None)
+        orderby = request.GET.get("orderby", None)
+
+        # Make a unique cache name
+        cache_name = self.__class__.__name__
+
+        for key, val in request.GET.iteritems():
+            cache_name = cache_name + str(key) + str(val)
+
+        for key, val in kwargs.iteritems():
+            cache_name = cache_name + str(key) + str(val)
+
+        # No special chars allowed in the cache name apart from dash
+        cache_name = re.sub(r'[^A-Za-z0-9-]', "", cache_name)
+        data = cache.get(cache_name)
+
+        if data:
+            return data
+
+        self.setup_columns(**kwargs)
+
+        if search:
+            self.apply_search(search)
+        if filters:
+            self.apply_filter(filters, **kwargs)
+        if orderby:
+            self.apply_orderby(orderby)
+
+        paginator = Paginator(self.queryset, limit)
+
+        try:
+            page = paginator.page(page_num)
+        except EmptyPage:
+            page = paginator.page(1)
+
+        data = {
+            'total' : self.queryset.count(),
+            'default_orderby' : self.default_orderby,
+            'columns' : self.columns,
+            'rows' : [],
+            'error' : "ok",
+        }
+
+        try:
+            for row in page.object_list:
+                #Use collection to maintain the order
+                required_data = collections.OrderedDict()
+
+                for col in self.columns:
+                    field = col['field_name']
+                    if not field:
+                        field = col['static_data_name']
+                    if not field:
+                        raise Exception("Must supply a field_name or static_data_name for column %s.%s" % (self.__class__.__name__,col))
+                    # Check if we need to process some static data
+                    if "static_data_name" in col and col['static_data_name']:
+                        required_data["static:%s" % col['static_data_name']] = self.render_static_data(col['static_data_template'], row)
+
+                        # Overwrite the field_name with static_data_name
+                        # so that this can be used as the html class name
+
+                        col['field_name'] = col['static_data_name']
+
+                    # compute the computation on the raw data if needed
+                    model_data = row
+                    if col['computation']:
+                        model_data = col['computation'](row)
+                    else:
+                        # Traverse to any foriegn key in the object hierachy
+                        for subfield in field.split("__"):
+                            if hasattr(model_data, subfield):
+                                model_data = getattr(model_data, subfield)
+                        # The field could be a function on the model so check
+                        # If it is then call it
+                        if isinstance(model_data, types.MethodType):
+                          model_data = model_data()
+
+                    required_data[col['field_name']] = model_data
+
+                data['rows'].append(required_data)
+
+        except FieldError:
+            # pass  it to the user - programming-error here
+            raise
+        data = json.dumps(data, indent=2, default=objtojson)
+        cache.set(cache_name, data, 60*30)
+
+        return data
+
+
+class ToasterTemplateView(TemplateView):
+    # renders a instance in a template, or returns the context as json
+    # the class-equivalent of the _template_renderer decorator for views
+
+    def __init__(self, *args, **kwargs):
+        super(ToasterTemplateView, self).__init__(*args, **kwargs)
+        self.context_entries = []
+
+    def get(self, *args, **kwargs):
+        if self.request.GET.get('format', None) == 'json':
+            from django.core.urlresolvers import reverse
+            from django.shortcuts import HttpResponse
+            from views import objtojson
+            from toastergui.templatetags.projecttags import json as jsonfilter
+
+            context = self.get_context_data(**kwargs)
+
+            for x in context.keys():
+                if x not in self.context_entries:
+                    del context[x]
+
+            context["error"] = "ok"
+
+            return HttpResponse(jsonfilter(context,  default=objtojson ),
+                            content_type = "application/json; charset=utf-8")
+
+        return super(ToasterTemplateView, self).get(*args, **kwargs)
+
+class ToasterTypeAhead(View):
+    """ A typeahead mechanism to support the front end typeahead widgets """
+    MAX_RESULTS = 6
+
+    class MissingFieldsException(Exception):
+        pass
+
+    def __init__(self, *args, **kwargs):
+        super(ToasterTypeAhead, self).__init__()
+
+    def get(self, request, *args, **kwargs):
+        def response(data):
+            return HttpResponse(json.dumps(data,
+                                           indent=2,
+                                           cls=DjangoJSONEncoder),
+                                content_type="application/json")
+
+        error = "ok"
+
+        search_term = request.GET.get("search", None)
+        if search_term == None:
+            # We got no search value so return empty reponse
+            return response({'error' : error , 'results': []})
+
+        try:
+            prj = Project.objects.get(pk=kwargs['pid'])
+        except KeyError:
+            prj = None
+
+        results = self.apply_search(search_term, prj, request)[:ToasterTypeAhead.MAX_RESULTS]
+
+        if len(results) > 0:
+            try:
+                self.validate_fields(results[0])
+            except MissingFieldsException as e:
+                error = e
+
+        data = { 'results' : results,
+                'error' : error,
+               }
+
+        return response(data)
+
+    def validate_fields(self, result):
+        if 'name' in result == False or 'detail' in result == False:
+            raise MissingFieldsException("name and detail are required fields")
+
+    def apply_search(self, search_term, prj):
+        """ Override this function to implement search. Return an array of
+        dictionaries with a minium of a name and detail field"""
+        pass