#
# BitBake Toaster Implementation
#
# Copyright (C) 2013        Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#

import re

from django.db.models import F, Q, Sum
from django.db import IntegrityError
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.http import urlencode
from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe
from orm.models import LogMessage, Variable, Package_Dependency, Package
from orm.models import Task_Dependency, Package_File
from orm.models import Target_Installed_Package, Target_File
from orm.models import TargetKernelFile, TargetSDKFile, Target_Image_File
from orm.models import BitbakeVersion, CustomImageRecipe

from django.urls import reverse, resolve
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.http import HttpResponseNotFound, JsonResponse
from django.utils import timezone
from datetime import timedelta, datetime
from toastergui.templatetags.projecttags import json as jsonfilter
from decimal import Decimal
import json
import os
from os.path import dirname
import mimetypes

import logging

logger = logging.getLogger("toaster")

# Project creation and managed build enable
project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER'))
is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC'))

class MimeTypeFinder(object):
    # setting this to False enables additional non-standard mimetypes
    # to be included in the guess
    _strict = False

    # returns the mimetype for a file path as a string,
    # or 'application/octet-stream' if the type couldn't be guessed
    @classmethod
    def get_mimetype(self, path):
        guess = mimetypes.guess_type(path, self._strict)
        guessed_type = guess[0]
        if guessed_type is None:
            guessed_type = 'application/octet-stream'
        return guessed_type

# single point to add global values into the context before rendering
def toaster_render(request, page, context):
    context['project_enable'] = project_enable
    context['project_specific'] = is_project_specific
    return render(request, page, context)


# all new sessions should come through the landing page;
# determine in which mode we are running in, and redirect appropriately
def landing(request):
    # in build mode, we redirect to the command-line builds page
    # if there are any builds for the default (cli builds) project
    default_project = Project.objects.get_or_create_default_project()
    default_project_builds = Build.objects.filter(project = default_project)

    # we only redirect to projects page if there is a user-generated project
    num_builds = Build.objects.all().count()
    user_projects = Project.objects.filter(is_default = False)
    has_user_project = user_projects.count() > 0

    if num_builds == 0 and has_user_project:
        return redirect(reverse('all-projects'), permanent = False)

    if num_builds > 0:
        return redirect(reverse('all-builds'), permanent = False)

    context = {'lvs_nos' : Layer_Version.objects.all().count()}

    return toaster_render(request, 'landing.html', context)

def objtojson(obj):
    from django.db.models.query import QuerySet
    from django.db.models import Model

    if isinstance(obj, datetime):
        return obj.isoformat()
    elif isinstance(obj, timedelta):
        return obj.total_seconds()
    elif isinstance(obj, QuerySet) or isinstance(obj, set):
        return list(obj)
    elif isinstance(obj, Decimal):
        return str(obj)
    elif type(obj).__name__ == "RelatedManager":
        return [x.pk for x in obj.all()]
    elif hasattr( obj, '__dict__') and isinstance(obj, Model):
        d = obj.__dict__
        nd = dict(d)
        for di in d.keys():
            if di.startswith("_"):
                del nd[di]
            elif isinstance(d[di], Model):
                nd[di] = d[di].pk
            elif isinstance(d[di], int) and hasattr(obj, "get_%s_display" % di):
                nd[di] = getattr(obj, "get_%s_display" % di)()
        return nd
    elif isinstance( obj, type(lambda x:x)):
        import inspect
        return inspect.getsourcelines(obj)[0]
    else:
        raise TypeError("Unserializable object %s (%s) of type %s" % ( obj, dir(obj), type(obj)))


def _lv_to_dict(prj, x = None):
    if x is None:
        def wrapper(x):
            return _lv_to_dict(prj, x)
        return wrapper

    return {"id": x.pk,
            "name": x.layer.name,
            "tooltip": "%s | %s" % (x.layer.vcs_url,x.get_vcs_reference()),
            "detail": "(%s" % x.layer.vcs_url + (")" if x.release is None else " | "+x.get_vcs_reference()+")"),
            "giturl": x.layer.vcs_url,
            "layerdetailurl" : reverse('layerdetails', args=(prj.id,x.pk)),
            "revision" : x.get_vcs_reference(),
           }


def _build_page_range(paginator, index = 1):
    try:
        page = paginator.page(index)
    except PageNotAnInteger:
        page = paginator.page(1)
    except  EmptyPage:
        page = paginator.page(paginator.num_pages)


    page.page_range = [page.number]
    crt_range = 0
    for i in range(1,5):
        if (page.number + i) <= paginator.num_pages:
            page.page_range = page.page_range + [ page.number + i]
            crt_range +=1
        if (page.number - i) > 0:
            page.page_range =  [page.number -i] + page.page_range
            crt_range +=1
        if crt_range == 4:
            break
    return page


def _verify_parameters(g, mandatory_parameters):
    miss = []
    for mp in mandatory_parameters:
        if not mp in g:
            miss.append(mp)
    if len(miss):
        return miss
    return None

def _redirect_parameters(view, g, mandatory_parameters, *args, **kwargs):
    try:
        from urllib import unquote, urlencode
    except ImportError:
        from urllib.parse import unquote, urlencode
    url = reverse(view, kwargs=kwargs)
    params = {}
    for i in g:
        params[i] = g[i]
    for i in mandatory_parameters:
        if not i in params:
            params[i] = unquote(str(mandatory_parameters[i]))

    return redirect(url + "?%s" % urlencode(params), permanent = False, **kwargs)

class RedirectException(Exception):
    def __init__(self, view, g, mandatory_parameters, *args, **kwargs):
        super(RedirectException, self).__init__()
        self.view = view
        self.g = g
        self.mandatory_parameters = mandatory_parameters
        self.oargs  = args
        self.okwargs = kwargs

    def get_redirect_response(self):
        return _redirect_parameters(self.view, self.g, self.mandatory_parameters, self.oargs, **self.okwargs)

FIELD_SEPARATOR = ":"
AND_VALUE_SEPARATOR = "!"
OR_VALUE_SEPARATOR = "|"
DESCENDING = "-"

def __get_q_for_val(name, value):
    if "OR" in value or "AND" in value:
        result = None
        for x in value.split("OR"):
             x = __get_q_for_val(name, x)
             result = result | x if result else x
        return result
    if "AND" in value:
        result = None
        for x in value.split("AND"):
            x = __get_q_for_val(name, x)
            result = result & x if result else x
        return result
    if value.startswith("NOT"):
        value = value[3:]
        if value == 'None':
            value = None
        kwargs = { name : value }
        return ~Q(**kwargs)
    else:
        if value == 'None':
            value = None
        kwargs = { name : value }
        return Q(**kwargs)

def _get_filtering_query(filter_string):

    search_terms = filter_string.split(FIELD_SEPARATOR)
    and_keys = search_terms[0].split(AND_VALUE_SEPARATOR)
    and_values = search_terms[1].split(AND_VALUE_SEPARATOR)

    and_query = None
    for kv in zip(and_keys, and_values):
        or_keys = kv[0].split(OR_VALUE_SEPARATOR)
        or_values = kv[1].split(OR_VALUE_SEPARATOR)
        query = None
        for key, val in zip(or_keys, or_values):
            x = __get_q_for_val(key, val)
            query = query | x if query else x

        and_query = and_query & query if and_query else query

    return and_query

def _get_toggle_order(request, orderkey, toggle_reverse = False):
    if toggle_reverse:
        return "%s:+" % orderkey if request.GET.get('orderby', "") == "%s:-" % orderkey else "%s:-" % orderkey
    else:
        return "%s:-" % orderkey if request.GET.get('orderby', "") == "%s:+" % orderkey else "%s:+" % orderkey

def _get_toggle_order_icon(request, orderkey):
    if request.GET.get('orderby', "") == "%s:+"%orderkey:
        return "down"
    elif request.GET.get('orderby', "") == "%s:-"%orderkey:
        return "up"
    else:
        return None

# we check that the input comes in a valid form that we can recognize
def _validate_input(field_input, model):

    invalid = None

    if field_input:
        field_input_list = field_input.split(FIELD_SEPARATOR)

        # Check we have only one colon
        if len(field_input_list) != 2:
            invalid = "We have an invalid number of separators: " + field_input + " -> " + str(field_input_list)
            return None, invalid

        # Check we have an equal number of terms both sides of the colon
        if len(field_input_list[0].split(AND_VALUE_SEPARATOR)) != len(field_input_list[1].split(AND_VALUE_SEPARATOR)):
            invalid = "Not all arg names got values"
            return None, invalid + str(field_input_list)

        # Check we are looking for a valid field
        valid_fields = [f.name for f in model._meta.get_fields()]
        for field in field_input_list[0].split(AND_VALUE_SEPARATOR):
            if True in [field.startswith(x) for x in valid_fields]:
                break
        else:
           return None, (field, valid_fields)

    return field_input, invalid

# uses search_allowed_fields in orm/models.py to create a search query
# for these fields with the supplied input text
def _get_search_results(search_term, queryset, model):
    search_object = None
    for st in search_term.split(" "):
        queries = None
        for field in model.search_allowed_fields:
            query = Q(**{field + '__icontains': st})
            queries = queries | query if queries else query

        search_object = search_object & queries if search_object else queries
    queryset = queryset.filter(search_object)

    return queryset


# function to extract the search/filter/ordering parameters from the request
# it uses the request and the model to validate input for the filter and orderby values
def _search_tuple(request, model):
    ordering_string, invalid = _validate_input(request.GET.get('orderby', ''), model)
    if invalid:
        raise BaseException("Invalid ordering model:" + str(model) + str(invalid))

    filter_string, invalid = _validate_input(request.GET.get('filter', ''), model)
    if invalid:
        raise BaseException("Invalid filter " + str(invalid))

    search_term = request.GET.get('search', '')
    return (filter_string, search_term, ordering_string)


# returns a lazy-evaluated queryset for a filter/search/order combination
def _get_queryset(model, queryset, filter_string, search_term, ordering_string, ordering_secondary=''):
    if filter_string:
        filter_query = _get_filtering_query(filter_string)
        queryset = queryset.filter(filter_query)
    else:
        queryset = queryset.all()

    if search_term:
        queryset = _get_search_results(search_term, queryset, model)

    if ordering_string:
        column, order = ordering_string.split(':')
        if column == re.sub('-','',ordering_secondary):
            ordering_secondary=''
        if order.lower() == DESCENDING:
            column = '-' + column
        if ordering_secondary:
            queryset = queryset.order_by(column, ordering_secondary)
        else:
            queryset = queryset.order_by(column)

    # insure only distinct records (e.g. from multiple search hits) are returned
    return queryset.distinct()

# returns the value of entries per page and the name of the applied sorting field.
# if the value is given explicitly as a GET parameter it will be the first selected,
# otherwise the cookie value will be used.
def _get_parameters_values(request, default_count, default_order):
    current_url = resolve(request.path_info).url_name
    pagesize = request.GET.get('count', request.session.get('%s_count' % current_url, default_count))
    orderby = request.GET.get('orderby', request.session.get('%s_orderby' % current_url, default_order))
    return (pagesize, orderby)


# set cookies for parameters. this is usefull in case parameters are set
# manually from the GET values of the link
def _set_parameters_values(pagesize, orderby, request):
    from django.urls import resolve
    current_url = resolve(request.path_info).url_name
    request.session['%s_count' % current_url] = pagesize
    request.session['%s_orderby' % current_url] =orderby

# date range: normalize GUI's dd/mm/yyyy to date object
def _normalize_input_date(date_str,default):
    date_str=re.sub('/', '-', date_str)
    # accept dd/mm/yyyy to d/m/yy
    try:
        date_in = datetime.strptime(date_str, "%d-%m-%Y")
    except ValueError:
        # courtesy try with two digit year
        try:
            date_in = datetime.strptime(date_str, "%d-%m-%y")
        except ValueError:
            return default
    date_in = date_in.replace(tzinfo=default.tzinfo)
    return date_in

# convert and normalize any received date range filter, for example:
# "completed_on__gte!completed_on__lt:01/03/2015!02/03/2015_daterange" to
# "completed_on__gte!completed_on__lt:2015-03-01!2015-03-02"
def _modify_date_range_filter(filter_string):
    # was the date range radio button selected?
    if 0 >  filter_string.find('_daterange'):
        return filter_string,''
    # normalize GUI dates to database format
    filter_string = filter_string.replace('_daterange','').replace(':','!');
    filter_list = filter_string.split('!');
    if 4 != len(filter_list):
        return filter_string
    today = timezone.localtime(timezone.now())
    date_id = filter_list[1]
    date_from = _normalize_input_date(filter_list[2],today)
    date_to = _normalize_input_date(filter_list[3],today)
    # swap dates if manually set dates are out of order
    if  date_to < date_from:
        date_to,date_from = date_from,date_to
    # convert to strings, make 'date_to' inclusive by moving to begining of next day
    date_from_str = date_from.strftime("%Y-%m-%d")
    date_to_str = (date_to+timedelta(days=1)).strftime("%Y-%m-%d")
    filter_string=filter_list[0]+'!'+filter_list[1]+':'+date_from_str+'!'+date_to_str
    daterange_selected = re.sub('__.*','', date_id)
    return filter_string,daterange_selected

def _add_daterange_context(queryset_all, request, daterange_list):
    # calculate the exact begining of local today and yesterday
    today_begin = timezone.localtime(timezone.now())
    yesterday_begin = today_begin - timedelta(days=1)
    # add daterange persistent
    context_date = {}
    context_date['last_date_from'] = request.GET.get('last_date_from',timezone.localtime(timezone.now()).strftime("%d/%m/%Y"))
    context_date['last_date_to'  ] = request.GET.get('last_date_to'  ,context_date['last_date_from'])
    # calculate the date ranges, avoid second sort for 'created'
    # fetch the respective max range from the database
    context_date['daterange_filter']=''
    for key in daterange_list:
        queryset_key = queryset_all.order_by(key)
        try:
            context_date['dateMin_'+key]=timezone.localtime(getattr(queryset_key.first(),key)).strftime("%d/%m/%Y")
        except AttributeError:
            context_date['dateMin_'+key]=timezone.localtime(timezone.now())
        try:
            context_date['dateMax_'+key]=timezone.localtime(getattr(queryset_key.last(),key)).strftime("%d/%m/%Y")
        except AttributeError:
            context_date['dateMax_'+key]=timezone.localtime(timezone.now())
    return context_date,today_begin,yesterday_begin


##
# build dashboard for a single build, coming in as argument
# Each build may contain multiple targets and each target
# may generate multiple image files. display them all.
#
def builddashboard( request, build_id ):
    template = "builddashboard.html"
    if Build.objects.filter( pk=build_id ).count( ) == 0 :
        return redirect( builds )
    build = Build.objects.get( pk = build_id );
    layerVersionId = Layer_Version.objects.filter( build = build_id );
    recipeCount = Recipe.objects.filter( layer_version__id__in = layerVersionId ).count( );
    tgts = Target.objects.filter( build_id = build_id ).order_by( 'target' );

    # set up custom target list with computed package and image data
    targets = []
    ntargets = 0

    # True if at least one target for this build has an SDK artifact
    # or image file
    has_artifacts = False

    for t in tgts:
        elem = {}
        elem['target'] = t

        target_has_images = False
        image_files = []

        npkg = 0
        pkgsz = 0
        package = None
        # Chunk the query to avoid "too many SQL variables" error
        package_set = t.target_installed_package_set.all()
        package_set_len = len(package_set)
        for ps_start in range(0,package_set_len,500):
            ps_stop = min(ps_start+500,package_set_len)
            for package in Package.objects.filter(id__in = [x.package_id for x in package_set[ps_start:ps_stop]]):
                pkgsz = pkgsz + package.size
                if package.installed_name:
                    npkg = npkg + 1
        elem['npkg'] = npkg
        elem['pkgsz'] = pkgsz
        ti = Target_Image_File.objects.filter(target_id = t.id)
        for i in ti:
            ndx = i.file_name.rfind('/')
            if ndx < 0:
                ndx = 0;
            f = i.file_name[ndx + 1:]
            image_files.append({
                'id': i.id,
                'path': f,
                'size': i.file_size,
                'suffix': i.suffix
            })
        if len(image_files) > 0:
            target_has_images = True
        elem['targetHasImages'] = target_has_images

        elem['imageFiles'] = image_files
        elem['target_kernel_artifacts'] = t.targetkernelfile_set.all()

        target_sdk_files = t.targetsdkfile_set.all()
        target_sdk_artifacts_count = target_sdk_files.count()
        elem['target_sdk_artifacts_count'] = target_sdk_artifacts_count
        elem['target_sdk_artifacts'] = target_sdk_files

        if target_has_images or target_sdk_artifacts_count > 0:
            has_artifacts = True

        targets.append(elem)

    ##
    # how many packages in this build - ignore anonymous ones
    #

    packageCount = 0
    packages = Package.objects.filter( build_id = build_id )
    for p in packages:
        if ( p.installed_name ):
            packageCount = packageCount + 1

    logmessages = list(LogMessage.objects.filter( build = build_id ))

    context = {
            'build'           : build,
            'project'         : build.project,
            'hasArtifacts'    : has_artifacts,
            'ntargets'        : ntargets,
            'targets'         : targets,
            'recipecount'     : recipeCount,
            'packagecount'    : packageCount,
            'logmessages'     : logmessages,
    }
    return toaster_render( request, template, context )



def generateCoveredList2( revlist = None ):
    if not revlist:
        revlist = []
    covered_list =  [ x for x in revlist if x.outcome == Task.OUTCOME_COVERED ]
    while len(covered_list):
        revlist =  [ x for x in revlist if x.outcome != Task.OUTCOME_COVERED ]
        if len(revlist) > 0:
            return revlist

        newlist = _find_task_revdep_list(covered_list)

        revlist = list(set(revlist + newlist))
        covered_list =  [ x for x in revlist if x.outcome == Task.OUTCOME_COVERED ]
    return revlist

def task( request, build_id, task_id ):
    template = "task.html"
    tasks_list = Task.objects.filter( pk=task_id )
    if tasks_list.count( ) == 0:
        return redirect( builds )
    task_object = tasks_list[ 0 ];
    dependencies = sorted(
        _find_task_dep( task_object ),
        key=lambda t:'%s_%s %s'%(t.recipe.name, t.recipe.version, t.task_name))
    reverse_dependencies = sorted(
        _find_task_revdep( task_object ),
        key=lambda t:'%s_%s %s'%( t.recipe.name, t.recipe.version, t.task_name ))
    coveredBy = '';
    if ( task_object.outcome == Task.OUTCOME_COVERED ):
#        _list = generateCoveredList( task )
        coveredBy = sorted(generateCoveredList2( _find_task_revdep( task_object ) ), key = lambda x: x.recipe.name)
    log_head = ''
    log_body = ''
    if task_object.outcome == task_object.OUTCOME_FAILED:
        pass

    uri_list= [ ]
    variables = Variable.objects.filter(build=build_id)
    v=variables.filter(variable_name='SSTATE_DIR')
    if v.count() > 0:
        uri_list.append(v[0].variable_value)
    v=variables.filter(variable_name='SSTATE_MIRRORS')
    if (v.count() > 0):
        for mirror in v[0].variable_value.split('\\n'):
            s=re.sub('.* ','',mirror.strip(' \t\n\r'))
            if len(s):
                uri_list.append(s)

    context = {
            'build'           : Build.objects.filter( pk = build_id )[ 0 ],
            'object'          : task_object,
            'task'            : task_object,
            'covered_by'      : coveredBy,
            'deps'            : dependencies,
            'rdeps'           : reverse_dependencies,
            'log_head'        : log_head,
            'log_body'        : log_body,
            'showing_matches' : False,
            'uri_list'        : uri_list,
            'task_in_tasks_table_pg':  int(task_object.order / 25) + 1
    }
    if request.GET.get( 'show_matches', "" ):
        context[ 'showing_matches' ] = True
        context[ 'matching_tasks' ] = Task.objects.filter(
            sstate_checksum=task_object.sstate_checksum ).filter(
            build__completed_on__lt=task_object.build.completed_on).exclude(
            order__isnull=True).exclude(outcome=Task.OUTCOME_NA).order_by('-build__completed_on')

    return toaster_render( request, template, context )

def recipe(request, build_id, recipe_id, active_tab="1"):
    template = "recipe.html"
    if Recipe.objects.filter(pk=recipe_id).count() == 0 :
        return redirect(builds)

    recipe_object = Recipe.objects.get(pk=recipe_id)
    layer_version = Layer_Version.objects.get(pk=recipe_object.layer_version_id)
    layer  = Layer.objects.get(pk=layer_version.layer_id)
    tasks_list  = Task.objects.filter(recipe_id = recipe_id, build_id = build_id).exclude(order__isnull=True).exclude(task_name__endswith='_setscene').exclude(outcome=Task.OUTCOME_NA)
    package_count = Package.objects.filter(recipe_id = recipe_id).filter(build_id = build_id).filter(size__gte=0).count()

    if active_tab != '1' and active_tab != '3' and active_tab != '4' :
        active_tab = '1'
    tab_states = {'1': '', '3': '', '4': ''}
    tab_states[active_tab] = 'active'

    context = {
            'build'   : Build.objects.get(pk=build_id),
            'object'  : recipe_object,
            'layer_version' : layer_version,
            'layer'   : layer,
            'tasks'   : tasks_list,
            'package_count' : package_count,
            'tab_states' : tab_states,
    }
    return toaster_render(request, template, context)

def recipe_packages(request, build_id, recipe_id):
    template = "recipe_packages.html"
    if Recipe.objects.filter(pk=recipe_id).count() == 0 :
        return redirect(builds)

    (pagesize, orderby) = _get_parameters_values(request, 10, 'name:+')
    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby': orderby }
    retval = _verify_parameters( request.GET, mandatory_parameters )
    if retval:
        return _redirect_parameters( 'recipe_packages', request.GET, mandatory_parameters, build_id = build_id, recipe_id = recipe_id)
    (filter_string, search_term, ordering_string) = _search_tuple(request, Package)

    recipe_object = Recipe.objects.get(pk=recipe_id)
    queryset = Package.objects.filter(recipe_id = recipe_id).filter(build_id = build_id).filter(size__gte=0)
    package_count = queryset.count()
    queryset = _get_queryset(Package, queryset, filter_string, search_term, ordering_string, 'name')

    packages = _build_page_range(Paginator(queryset, pagesize),request.GET.get('page', 1))

    context = {
            'build'   : Build.objects.get(pk=build_id),
            'recipe'  : recipe_object,
            'objects'  : packages,
            'object_count' : package_count,
            'tablecols':[
                {
                    'name':'Package',
                    'orderfield': _get_toggle_order(request,"name"),
                    'ordericon': _get_toggle_order_icon(request,"name"),
                    'orderkey': "name",
                },
                {
                    'name':'Version',
                },
                {
                    'name':'Size',
                    'orderfield': _get_toggle_order(request,"size", True),
                    'ordericon': _get_toggle_order_icon(request,"size"),
                    'orderkey': 'size',
                    'dclass': 'sizecol span2',
                },
           ]
       }
    response = toaster_render(request, template, context)
    _set_parameters_values(pagesize, orderby, request)
    return response

from django.http import HttpResponse
def xhr_dirinfo(request, build_id, target_id):
    top = request.GET.get('start', '/')
    return HttpResponse(_get_dir_entries(build_id, target_id, top), content_type = "application/json")

from django.utils.functional import Promise
from django.utils.encoding import force_text
class LazyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Promise):
            return force_text(obj)
        return super(LazyEncoder, self).default(obj)

from toastergui.templatetags.projecttags import filtered_filesizeformat
import os
def _get_dir_entries(build_id, target_id, start):
    node_str = {
        Target_File.ITYPE_REGULAR   : '-',
        Target_File.ITYPE_DIRECTORY : 'd',
        Target_File.ITYPE_SYMLINK   : 'l',
        Target_File.ITYPE_SOCKET    : 's',
        Target_File.ITYPE_FIFO      : 'p',
        Target_File.ITYPE_CHARACTER : 'c',
        Target_File.ITYPE_BLOCK     : 'b',
    }
    response = []
    objects  = Target_File.objects.filter(target__exact=target_id, directory__path=start)
    target_packages = Target_Installed_Package.objects.filter(target__exact=target_id).values_list('package_id', flat=True)
    for o in objects:
        # exclude root inode '/'
        if o.path == '/':
            continue
        try:
            entry = {}
            entry['parent'] = start
            entry['name'] = os.path.basename(o.path)
            entry['fullpath'] = o.path

            # set defaults, not all dentries have packages
            entry['installed_package'] = None
            entry['package_id'] = None
            entry['package'] = None
            entry['link_to'] = None
            if o.inodetype == Target_File.ITYPE_DIRECTORY:
                entry['isdir'] = 1
                # is there content in directory
                entry['childcount'] = Target_File.objects.filter(target__exact=target_id, directory__path=o.path).all().count()
            else:
                entry['isdir'] = 0

                # resolve the file to get the package from the resolved file
                resolved_id = o.sym_target_id
                resolved_path = o.path
                if target_packages.count():
                    while resolved_id != "" and resolved_id is not None:
                        tf = Target_File.objects.get(pk=resolved_id)
                        resolved_path = tf.path
                        resolved_id = tf.sym_target_id

                    thisfile=Package_File.objects.all().filter(path__exact=resolved_path, package_id__in=target_packages)
                    if thisfile.count():
                        p = Package.objects.get(pk=thisfile[0].package_id)
                        entry['installed_package'] = p.installed_name
                        entry['package_id'] = str(p.id)
                        entry['package'] = p.name
                # don't use resolved path from above, show immediate link-to
                if o.sym_target_id != "" and o.sym_target_id is not None:
                    entry['link_to'] = Target_File.objects.get(pk=o.sym_target_id).path
            entry['size'] = filtered_filesizeformat(o.size)
            if entry['link_to'] is not None:
                entry['permission'] = node_str[o.inodetype] + o.permission
            else:
                entry['permission'] = node_str[o.inodetype] + o.permission
            entry['owner'] = o.owner
            entry['group'] = o.group
            response.append(entry)

        except Exception as e:
            print("Exception ", e)
            traceback.print_exc()

    # sort by directories first, then by name
    rsorted = sorted(response, key=lambda entry :  entry['name'])
    rsorted = sorted(rsorted, key=lambda entry :  entry['isdir'], reverse=True)
    return json.dumps(rsorted, cls=LazyEncoder).replace('</', '<\\/')

def dirinfo(request, build_id, target_id, file_path=None):
    template = "dirinfo.html"
    objects = _get_dir_entries(build_id, target_id, '/')
    packages_sum = Package.objects.filter(id__in=Target_Installed_Package.objects.filter(target_id=target_id).values('package_id')).aggregate(Sum('installed_size'))
    dir_list = None
    if file_path is not None:
        """
        Link from the included package detail file list page and is
        requesting opening the dir info to a specific file path.
        Provide the list of directories to expand and the full path to
        highlight in the page.
        """
        # Aassume target's path separator matches host's, that is, os.sep
        sep = os.sep
        dir_list = []
        head = file_path
        while head != sep:
            (head, tail) = os.path.split(head)
            if head != sep:
                dir_list.insert(0, head)

    build = Build.objects.get(pk=build_id)

    context = { 'build': build,
                'project': build.project,
                'target': Target.objects.get(pk=target_id),
                'packages_sum': packages_sum['installed_size__sum'],
                'objects': objects,
                'dir_list': dir_list,
                'file_path': file_path,
              }
    return toaster_render(request, template, context)

def _find_task_dep(task_object):
    tdeps = Task_Dependency.objects.filter(task=task_object).filter(depends_on__order__gt=0)
    tdeps = tdeps.exclude(depends_on__outcome=Task.OUTCOME_NA).select_related("depends_on")
    return [x.depends_on for x in tdeps]

def _find_task_revdep(task_object):
    tdeps = Task_Dependency.objects.filter(depends_on=task_object).filter(task__order__gt=0)
    tdeps = tdeps.exclude(task__outcome = Task.OUTCOME_NA).select_related("task", "task__recipe", "task__build")

    # exclude self-dependencies to prevent infinite dependency loop
    # in generateCoveredList2()
    tdeps = tdeps.exclude(task=task_object)

    return [tdep.task for tdep in tdeps]

def _find_task_revdep_list(tasklist):
    tdeps = Task_Dependency.objects.filter(depends_on__in=tasklist).filter(task__order__gt=0)
    tdeps = tdeps.exclude(task__outcome=Task.OUTCOME_NA).select_related("task", "task__recipe", "task__build")

    # exclude self-dependencies to prevent infinite dependency loop
    # in generateCoveredList2()
    tdeps = tdeps.exclude(task=F('depends_on'))

    return [tdep.task for tdep in tdeps]

def _find_task_provider(task_object):
    task_revdeps = _find_task_revdep(task_object)
    for tr in task_revdeps:
        if tr.outcome != Task.OUTCOME_COVERED:
            return tr
    for tr in task_revdeps:
        trc = _find_task_provider(tr)
        if trc is not None:
            return trc
    return None

def configuration(request, build_id):
    template = 'configuration.html'

    var_names = ('BB_VERSION', 'BUILD_SYS', 'NATIVELSBSTRING', 'TARGET_SYS',
                 'MACHINE', 'DISTRO', 'DISTRO_VERSION', 'TUNE_FEATURES', 'TARGET_FPU')
    context = dict(Variable.objects.filter(build=build_id, variable_name__in=var_names)\
                                           .values_list('variable_name', 'variable_value'))
    build = Build.objects.get(pk=build_id)
    context.update({'objectname': 'configuration',
                    'object_search_display':'variables',
                    'filter_search_display':'variables',
                    'build': build,
                    'project': build.project,
                    'targets': Target.objects.filter(build=build_id)})
    return toaster_render(request, template, context)


def configvars(request, build_id):
    template = 'configvars.html'
    (pagesize, orderby) = _get_parameters_values(request, 100, 'variable_name:+')
    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby' : orderby, 'filter' : 'description__regex:.+' }
    retval = _verify_parameters( request.GET, mandatory_parameters )
    (filter_string, search_term, ordering_string) = _search_tuple(request, Variable)
    if retval:
        # if new search, clear the default filter
        if search_term and len(search_term):
            mandatory_parameters['filter']=''
        return _redirect_parameters( 'configvars', request.GET, mandatory_parameters, build_id = build_id)

    queryset = Variable.objects.filter(build=build_id).exclude(variable_name__istartswith='B_').exclude(variable_name__istartswith='do_')
    queryset_with_search =  _get_queryset(Variable, queryset, None, search_term, ordering_string, 'variable_name').exclude(variable_value='',vhistory__file_name__isnull=True)
    queryset = _get_queryset(Variable, queryset, filter_string, search_term, ordering_string, 'variable_name')
    # remove records where the value is empty AND there are no history files
    queryset = queryset.exclude(variable_value='',vhistory__file_name__isnull=True)

    variables = _build_page_range(Paginator(queryset, pagesize), request.GET.get('page', 1))

    # show all matching files (not just the last one)
    file_filter= search_term + ":"
    if filter_string.find('/conf/') > 0:
        file_filter += 'conf/(local|bblayers).conf'
    if filter_string.find('conf/machine/') > 0:
        file_filter += 'conf/machine/'
    if filter_string.find('conf/distro/') > 0:
        file_filter += 'conf/distro/'
    if filter_string.find('/bitbake.conf') > 0:
        file_filter += '/bitbake.conf'
    build_dir=re.sub("/tmp/log/.*","",Build.objects.get(pk=build_id).cooker_log_path)

    build = Build.objects.get(pk=build_id)

    context = {
                'objectname': 'configvars',
                'object_search_display':'BitBake variables',
                'filter_search_display':'variables',
                'file_filter': file_filter,
                'build': build,
                'project': build.project,
                'objects' : variables,
                'total_count':queryset_with_search.count(),
                'default_orderby' : 'variable_name:+',
                'search_term':search_term,
            # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
                'tablecols' : [
                {'name': 'Variable',
                 'qhelp': "BitBake is a generic task executor that considers a list of tasks with dependencies and handles metadata that consists of variables in a certain format that get passed to the tasks",
                 'orderfield': _get_toggle_order(request, "variable_name"),
                 'ordericon':_get_toggle_order_icon(request, "variable_name"),
                },
                {'name': 'Value',
                 'qhelp': "The value assigned to the variable",
                },
                {'name': 'Set in file',
                 'qhelp': "The last configuration file that touched the variable value",
                 'clclass': 'file', 'hidden' : 0,
                 'orderkey' : 'vhistory__file_name',
                 'filter' : {
                    'class' : 'vhistory__file_name',
                    'label': 'Show:',
                    'options' : [
                               ('Local configuration variables', 'vhistory__file_name__contains:'+build_dir+'/conf/',queryset_with_search.filter(vhistory__file_name__contains=build_dir+'/conf/').count(), 'Select this filter to see variables set by the <code>local.conf</code> and <code>bblayers.conf</code> configuration files inside the <code>/build/conf/</code> directory'),
                               ('Machine configuration variables', 'vhistory__file_name__contains:conf/machine/',queryset_with_search.filter(vhistory__file_name__contains='conf/machine').count(), 'Select this filter to see variables set by the configuration file(s) inside your layers <code>/conf/machine/</code> directory'),
                               ('Distro configuration variables', 'vhistory__file_name__contains:conf/distro/',queryset_with_search.filter(vhistory__file_name__contains='conf/distro').count(), 'Select this filter to see variables set by the configuration file(s) inside your layers <code>/conf/distro/</code> directory'),
                               ('Layer configuration variables', 'vhistory__file_name__contains:conf/layer.conf',queryset_with_search.filter(vhistory__file_name__contains='conf/layer.conf').count(), 'Select this filter to see variables set by the <code>layer.conf</code> configuration file inside your layers'),
                               ('bitbake.conf variables', 'vhistory__file_name__contains:/bitbake.conf',queryset_with_search.filter(vhistory__file_name__contains='/bitbake.conf').count(), 'Select this filter to see variables set by the <code>bitbake.conf</code> configuration file'),
                               ]
                             },
                },
                {'name': 'Description',
                 'qhelp': "A brief explanation of the variable",
                 'clclass': 'description', 'hidden' : 0,
                 'dclass': "span4",
                 'filter' : {
                    'class' : 'description',
                    'label': 'Show:',
                    'options' : [
                               ('Variables with description', 'description__regex:.+', queryset_with_search.filter(description__regex='.+').count(), 'We provide descriptions for the most common BitBake variables. The list of descriptions lives in <code>meta/conf/documentation.conf</code>'),
                               ]
                            },
                },
                ],
            }

    response = toaster_render(request, template, context)
    _set_parameters_values(pagesize, orderby, request)
    return response

def bfile(request, build_id, package_id):
    template = 'bfile.html'
    files = Package_File.objects.filter(package = package_id)
    build = Build.objects.get(pk=build_id)
    context = {
        'build': build,
        'project': build.project,
        'objects' : files
    }
    return toaster_render(request, template, context)


# A set of dependency types valid for both included and built package views
OTHER_DEPENDS_BASE = [
    Package_Dependency.TYPE_RSUGGESTS,
    Package_Dependency.TYPE_RPROVIDES,
    Package_Dependency.TYPE_RREPLACES,
    Package_Dependency.TYPE_RCONFLICTS,
    ]

# value for invalid row id
INVALID_KEY = -1

"""
Given a package id, target_id retrieves two sets of this image and package's
dependencies.  The return value is a dictionary consisting of two other
lists: a list of 'runtime' dependencies, that is, having RDEPENDS
values in source package's recipe, and a list of other dependencies, that is
the list of possible recipe variables as found in OTHER_DEPENDS_BASE plus
the RRECOMMENDS or TRECOMMENDS value.
The lists are built in the sort order specified for the package runtime
dependency views.
"""
def _get_package_dependencies(package_id, target_id = INVALID_KEY):
    runtime_deps = []
    other_deps = []
    other_depends_types = OTHER_DEPENDS_BASE

    if target_id != INVALID_KEY :
        rdepends_type = Package_Dependency.TYPE_TRDEPENDS
        other_depends_types +=  [Package_Dependency.TYPE_TRECOMMENDS]
    else :
        rdepends_type = Package_Dependency.TYPE_RDEPENDS
        other_depends_types += [Package_Dependency.TYPE_RRECOMMENDS]

    package = Package.objects.get(pk=package_id)
    if target_id != INVALID_KEY :
        alldeps = package.package_dependencies_source.filter(target_id__exact = target_id)
    else :
        alldeps = package.package_dependencies_source.all()
    for idep in alldeps:
        dep_package = Package.objects.get(pk=idep.depends_on_id)
        dep_entry = Package_Dependency.DEPENDS_DICT[idep.dep_type]
        if dep_package.version == '' :
            version = ''
        else :
            version = dep_package.version + "-" + dep_package.revision
        installed = False
        if target_id != INVALID_KEY :
            if Target_Installed_Package.objects.filter(target_id__exact = target_id, package_id__exact = dep_package.id).count() > 0:
                installed = True
        dep =   {
                'name' : dep_package.name,
                'version' : version,
                'size' : dep_package.size,
                'dep_type' : idep.dep_type,
                'dep_type_display' : dep_entry[0].capitalize(),
                'dep_type_help' : dep_entry[1] % (dep_package.name, package.name),
                'depends_on_id' : dep_package.id,
                'installed' : installed,
                }

        if target_id != INVALID_KEY:
                dep['alias'] = _get_package_alias(dep_package)

        if idep.dep_type == rdepends_type :
            runtime_deps.append(dep)
        elif idep.dep_type in other_depends_types :
            other_deps.append(dep)

    rdep_sorted = sorted(runtime_deps, key=lambda k: k['name'])
    odep_sorted = sorted(
            sorted(other_deps, key=lambda k: k['name']),
            key=lambda k: k['dep_type'])
    retvalues = {'runtime_deps' : rdep_sorted, 'other_deps' : odep_sorted}
    return retvalues

# Return the count of packages dependent on package for this target_id image
def _get_package_reverse_dep_count(package, target_id):
    return package.package_dependencies_target.filter(target_id__exact=target_id, dep_type__exact = Package_Dependency.TYPE_TRDEPENDS).count()

# Return the count of the packages that this package_id is dependent on.
# Use one of the two RDEPENDS types, either TRDEPENDS if the package was
# installed, or else RDEPENDS if only built.
def _get_package_dependency_count(package, target_id, is_installed):
    if is_installed :
        return package.package_dependencies_source.filter(target_id__exact = target_id,
            dep_type__exact = Package_Dependency.TYPE_TRDEPENDS).count()
    else :
        return package.package_dependencies_source.filter(dep_type__exact = Package_Dependency.TYPE_RDEPENDS).count()

def _get_package_alias(package):
    alias = package.installed_name
    if alias is not None and alias != '' and alias != package.name:
        return alias
    else:
        return ''

def _get_fullpackagespec(package):
    r = package.name
    version_good = package.version is not None and  package.version != ''
    revision_good = package.revision is not None and package.revision != ''
    if version_good or revision_good:
        r += '_'
        if version_good:
            r += package.version
            if revision_good:
                r += '-'
        if revision_good:
            r += package.revision
    return r

def package_built_detail(request, build_id, package_id):
    template = "package_built_detail.html"
    if Build.objects.filter(pk=build_id).count() == 0 :
        return redirect(builds)

    # follow convention for pagination w/ search although not used for this view
    queryset = Package_File.objects.filter(package_id__exact=package_id)
    (pagesize, orderby) = _get_parameters_values(request, 25, 'path:+')
    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby' : orderby }
    retval = _verify_parameters( request.GET, mandatory_parameters )
    if retval:
        return _redirect_parameters( 'package_built_detail', request.GET, mandatory_parameters, build_id = build_id, package_id = package_id)

    (filter_string, search_term, ordering_string) = _search_tuple(request, Package_File)
    paths = _get_queryset(Package_File, queryset, filter_string, search_term, ordering_string, 'path')

    package = Package.objects.get(pk=package_id)
    package.fullpackagespec = _get_fullpackagespec(package)
    context = {
            'build' : Build.objects.get(pk=build_id),
            'package' : package,
            'dependency_count' : _get_package_dependency_count(package, -1, False),
            'objects' : paths,
            'tablecols':[
                {
                    'name':'File',
                    'orderfield': _get_toggle_order(request, "path"),
                    'ordericon':_get_toggle_order_icon(request, "path"),
                },
                {
                    'name':'Size',
                    'orderfield': _get_toggle_order(request, "size", True),
                    'ordericon':_get_toggle_order_icon(request, "size"),
                    'dclass': 'sizecol span2',
                },
            ]
    }
    if paths.all().count() < 2:
        context['disable_sort'] = True;

    response = toaster_render(request, template, context)
    _set_parameters_values(pagesize, orderby, request)
    return response

def package_built_dependencies(request, build_id, package_id):
    template = "package_built_dependencies.html"
    if Build.objects.filter(pk=build_id).count() == 0 :
         return redirect(builds)

    package = Package.objects.get(pk=package_id)
    package.fullpackagespec = _get_fullpackagespec(package)
    dependencies = _get_package_dependencies(package_id)
    context = {
            'build' : Build.objects.get(pk=build_id),
            'package' : package,
            'runtime_deps' : dependencies['runtime_deps'],
            'other_deps' :   dependencies['other_deps'],
            'dependency_count' : _get_package_dependency_count(package, -1,  False)
    }
    return toaster_render(request, template, context)


def package_included_detail(request, build_id, target_id, package_id):
    template = "package_included_detail.html"
    if Build.objects.filter(pk=build_id).count() == 0 :
        return redirect(builds)

    # follow convention for pagination w/ search although not used for this view
    (pagesize, orderby) = _get_parameters_values(request, 25, 'path:+')
    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby' : orderby }
    retval = _verify_parameters( request.GET, mandatory_parameters )
    if retval:
        return _redirect_parameters( 'package_included_detail', request.GET, mandatory_parameters, build_id = build_id, target_id = target_id, package_id = package_id)
    (filter_string, search_term, ordering_string) = _search_tuple(request, Package_File)

    queryset = Package_File.objects.filter(package_id__exact=package_id)
    paths = _get_queryset(Package_File, queryset, filter_string, search_term, ordering_string, 'path')

    package = Package.objects.get(pk=package_id)
    package.fullpackagespec = _get_fullpackagespec(package)
    package.alias = _get_package_alias(package)
    target = Target.objects.get(pk=target_id)
    context = {
            'build' : Build.objects.get(pk=build_id),
            'target'  : target,
            'package' : package,
            'reverse_count' : _get_package_reverse_dep_count(package, target_id),
            'dependency_count' : _get_package_dependency_count(package, target_id, True),
            'objects': paths,
            'tablecols':[
                {
                    'name':'File',
                    'orderfield': _get_toggle_order(request, "path"),
                    'ordericon':_get_toggle_order_icon(request, "path"),
                },
                {
                    'name':'Size',
                    'orderfield': _get_toggle_order(request, "size", True),
                    'ordericon':_get_toggle_order_icon(request, "size"),
                    'dclass': 'sizecol span2',
                },
            ]
    }
    if paths.all().count() < 2:
        context['disable_sort'] = True
    response = toaster_render(request, template, context)
    _set_parameters_values(pagesize, orderby, request)
    return response

def package_included_dependencies(request, build_id, target_id, package_id):
    template = "package_included_dependencies.html"
    if Build.objects.filter(pk=build_id).count() == 0 :
        return redirect(builds)

    package = Package.objects.get(pk=package_id)
    package.fullpackagespec = _get_fullpackagespec(package)
    package.alias = _get_package_alias(package)
    target = Target.objects.get(pk=target_id)

    dependencies = _get_package_dependencies(package_id, target_id)
    context = {
            'build' : Build.objects.get(pk=build_id),
            'package' : package,
            'target' : target,
            'runtime_deps' : dependencies['runtime_deps'],
            'other_deps' :   dependencies['other_deps'],
            'reverse_count' : _get_package_reverse_dep_count(package, target_id),
            'dependency_count' : _get_package_dependency_count(package, target_id, True)
    }
    return toaster_render(request, template, context)

def package_included_reverse_dependencies(request, build_id, target_id, package_id):
    template = "package_included_reverse_dependencies.html"
    if Build.objects.filter(pk=build_id).count() == 0 :
        return redirect(builds)

    (pagesize, orderby) = _get_parameters_values(request, 25, 'package__name:+')
    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby': orderby }
    retval = _verify_parameters( request.GET, mandatory_parameters )
    if retval:
        return _redirect_parameters( 'package_included_reverse_dependencies', request.GET, mandatory_parameters, build_id = build_id, target_id = target_id, package_id = package_id)
    (filter_string, search_term, ordering_string) = _search_tuple(request, Package_File)

    queryset = Package_Dependency.objects.select_related('depends_on').filter(depends_on=package_id, target_id=target_id, dep_type=Package_Dependency.TYPE_TRDEPENDS)
    objects = _get_queryset(Package_Dependency, queryset, filter_string, search_term, ordering_string, 'package__name')

    package = Package.objects.get(pk=package_id)
    package.fullpackagespec = _get_fullpackagespec(package)
    package.alias = _get_package_alias(package)
    target = Target.objects.get(pk=target_id)
    for o in objects:
        if o.package.version != '':
            o.package.version += '-' + o.package.revision
        o.alias = _get_package_alias(o.package)
    context = {
            'build' : Build.objects.get(pk=build_id),
            'package' : package,
            'target' : target,
            'objects' : objects,
            'reverse_count' : _get_package_reverse_dep_count(package, target_id),
            'dependency_count' : _get_package_dependency_count(package, target_id, True),
            'tablecols':[
                {
                    'name':'Package',
                    'orderfield': _get_toggle_order(request, "package__name"),
                    'ordericon': _get_toggle_order_icon(request, "package__name"),
                },
                {
                    'name':'Version',
                },
                {
                    'name':'Size',
                    'orderfield': _get_toggle_order(request, "package__size", True),
                    'ordericon': _get_toggle_order_icon(request, "package__size"),
                    'dclass': 'sizecol span2',
                },
            ]
    }
    if objects.all().count() < 2:
        context['disable_sort'] = True
    response = toaster_render(request, template, context)
    _set_parameters_values(pagesize, orderby, request)
    return response

def image_information_dir(request, build_id, target_id, packagefile_id):
    # stubbed for now
    return redirect(builds)
    # the context processor that supplies data used across all the pages

# a context processor which runs on every request; this provides the
# projects and non_cli_projects (i.e. projects created by the user)
# variables referred to in templates, which used to determine the
# visibility of UI elements like the "New build" button
def managedcontextprocessor(request):
    projects = Project.objects.all()
    ret = {
        "projects": projects,
        "non_cli_projects": projects.exclude(is_default=True),
        "DEBUG" : toastermain.settings.DEBUG,
        "TOASTER_BRANCH": toastermain.settings.TOASTER_BRANCH,
        "TOASTER_REVISION" : toastermain.settings.TOASTER_REVISION,
    }
    return ret

# REST-based API calls to return build/building status to external Toaster
# managers and aggregators via JSON

def _json_build_status(build_id,extend):
    build_stat = None
    try:
        build = Build.objects.get( pk = build_id )
        build_stat = {}
        build_stat['id'] = build.id
        build_stat['name'] = build.build_name
        build_stat['machine'] = build.machine
        build_stat['distro'] = build.distro
        build_stat['start'] = build.started_on
        # look up target name
        target= Target.objects.get( build = build )
        if target:
            if target.task:
                build_stat['target'] = '%s:%s' % (target.target,target.task)
            else:
                build_stat['target'] = '%s' % (target.target)
        else:
            build_stat['target'] = ''
        # look up project name
        project = Project.objects.get( build = build )
        if project:
            build_stat['project'] = project.name
        else:
            build_stat['project'] = ''
        if Build.IN_PROGRESS == build.outcome:
            now = timezone.now()
            timediff = now - build.started_on
            build_stat['seconds']='%.3f' % timediff.total_seconds()
            build_stat['clone']='%d:%d' % (build.repos_cloned,build.repos_to_clone)
            build_stat['parse']='%d:%d' % (build.recipes_parsed,build.recipes_to_parse)
            tf = Task.objects.filter(build = build)
            tfc = tf.count()
            if tfc > 0:
                tfd = tf.exclude(order__isnull=True).count()
            else:
                tfd = 0
            build_stat['task']='%d:%d' % (tfd,tfc)
        else:
            build_stat['outcome'] = build.get_outcome_text()
            timediff = build.completed_on - build.started_on
            build_stat['seconds']='%.3f' % timediff.total_seconds()
            build_stat['stop'] = build.completed_on
            messages = LogMessage.objects.all().filter(build = build)
            errors = len(messages.filter(level=LogMessage.ERROR) |
                 messages.filter(level=LogMessage.EXCEPTION) |
                 messages.filter(level=LogMessage.CRITICAL))
            build_stat['errors'] = errors
            warnings = len(messages.filter(level=LogMessage.WARNING))
            build_stat['warnings'] = warnings
        if extend:
            build_stat['cooker_log'] = build.cooker_log_path
    except Exception as e:
        build_state = str(e)
    return build_stat

def json_builds(request):
    build_table = []
    builds = []
    try:
        builds = Build.objects.exclude(outcome=Build.IN_PROGRESS).order_by("-started_on")
        for build in builds:
            build_table.append(_json_build_status(build.id,False))
    except Exception as e:
        build_table = str(e)
    return JsonResponse({'builds' : build_table, 'count' : len(builds)})

def json_building(request):
    build_table = []
    builds = []
    try:
        builds = Build.objects.filter(outcome=Build.IN_PROGRESS).order_by("-started_on")
        for build in builds:
            build_table.append(_json_build_status(build.id,False))
    except Exception as e:
        build_table = str(e)
    return JsonResponse({'building' : build_table, 'count' : len(builds)})

def json_build(request,build_id):
    return JsonResponse({'build' : _json_build_status(build_id,True)})


import toastermain.settings

from orm.models import Project, ProjectLayer, ProjectVariable
from bldcontrol.models import  BuildEnvironment

# we have a set of functions if we're in managed mode, or
# a default "page not available" simple functions for interactive mode

if True:
    from django.contrib.auth.models import User
    from django.contrib.auth import authenticate, login

    from orm.models import LayerSource, ToasterSetting, Release

    import traceback

    class BadParameterException(Exception):
        ''' The exception raised on invalid POST requests '''
        pass

    # new project
    def newproject(request):
        if not project_enable:
            return redirect( landing )

        template = "newproject.html"
        context = {
            'email': request.user.email if request.user.is_authenticated else '',
            'username': request.user.username if request.user.is_authenticated else '',
            'releases': Release.objects.order_by("description"),
        }

        try:
            context['defaultbranch'] = ToasterSetting.objects.get(name = "DEFAULT_RELEASE").value
        except ToasterSetting.DoesNotExist:
            pass

        if request.method == "GET":
            # render new project page
            return toaster_render(request, template, context)
        elif request.method == "POST":
            mandatory_fields = ['projectname', 'ptype']
            try:
                ptype = request.POST.get('ptype')
                if ptype == "import":
                    mandatory_fields.append('importdir')
                else:
                    mandatory_fields.append('projectversion')
                # make sure we have values for all mandatory_fields
                missing = [field for field in mandatory_fields if len(request.POST.get(field, '')) == 0]
                if missing:
                    # set alert for missing fields
                    raise BadParameterException("Fields missing: %s" % ", ".join(missing))

                if not request.user.is_authenticated:
                    user = authenticate(username = request.POST.get('username', '_anonuser'), password = 'nopass')
                    if user is None:
                        user = User.objects.create_user(username = request.POST.get('username', '_anonuser'), email = request.POST.get('email', ''), password = "nopass")

                        user = authenticate(username = user.username, password = 'nopass')
                    login(request, user)

                #  save the project
                if ptype == "import":
                    if not os.path.isdir('%s/conf' % request.POST['importdir']):
                        raise BadParameterException("Bad path or missing 'conf' directory (%s)" % request.POST['importdir'])
                    from django.core import management
                    management.call_command('buildimport', '--command=import', '--name=%s' % request.POST['projectname'], '--path=%s' % request.POST['importdir'], interactive=False)
                    prj = Project.objects.get(name = request.POST['projectname'])
                    prj.merged_attr = True
                    prj.save()
                else:
                    release = Release.objects.get(pk = request.POST.get('projectversion', None ))
                    prj = Project.objects.create_project(name = request.POST['projectname'], release = release)
                    prj.user_id = request.user.pk
                    if 'mergeattr' == request.POST.get('mergeattr', ''):
                        prj.merged_attr = True
                    prj.save()

                return redirect(reverse(project, args=(prj.pk,)) + "?notify=new-project")

            except (IntegrityError, BadParameterException) as e:
                # fill in page with previously submitted values
                for field in mandatory_fields:
                    context.__setitem__(field, request.POST.get(field, "-- missing"))
                if isinstance(e, IntegrityError) and "username" in str(e):
                    context['alert'] = "Your chosen username is already used"
                else:
                    context['alert'] = str(e)
                return toaster_render(request, template, context)

        raise Exception("Invalid HTTP method for this page")

    # new project
    def newproject_specific(request, pid):
        if not project_enable:
            return redirect( landing )

        project = Project.objects.get(pk=pid)
        template = "newproject_specific.html"
        context = {
            'email': request.user.email if request.user.is_authenticated else '',
            'username': request.user.username if request.user.is_authenticated else '',
            'releases': Release.objects.order_by("description"),
            'projectname': project.name,
            'project_pk': project.pk,
        }

        # WORKAROUND: if we already know release, redirect 'newproject_specific' to 'project_specific'
        if '1' == project.get_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE'):
            return redirect(reverse(project_specific, args=(project.pk,)))

        try:
            context['defaultbranch'] = ToasterSetting.objects.get(name = "DEFAULT_RELEASE").value
        except ToasterSetting.DoesNotExist:
            pass

        if request.method == "GET":
            # render new project page
            return toaster_render(request, template, context)
        elif request.method == "POST":
            mandatory_fields = ['projectname', 'ptype']
            try:
                ptype = request.POST.get('ptype')
                if ptype == "build":
                    mandatory_fields.append('projectversion')
                # make sure we have values for all mandatory_fields
                missing = [field for field in mandatory_fields if len(request.POST.get(field, '')) == 0]
                if missing:
                    # set alert for missing fields
                    raise BadParameterException("Fields missing: %s" % ", ".join(missing))

                if not request.user.is_authenticated:
                    user = authenticate(username = request.POST.get('username', '_anonuser'), password = 'nopass')
                    if user is None:
                        user = User.objects.create_user(username = request.POST.get('username', '_anonuser'), email = request.POST.get('email', ''), password = "nopass")

                        user = authenticate(username = user.username, password = 'nopass')
                    login(request, user)

                #  save the project
                if ptype == "analysis":
                    release = None
                else:
                    release = Release.objects.get(pk = request.POST.get('projectversion', None ))

                prj = Project.objects.create_project(name = request.POST['projectname'], release = release, existing_project = project)
                prj.user_id = request.user.pk
                prj.save()
                return redirect(reverse(project_specific, args=(prj.pk,)) + "?notify=new-project")

            except (IntegrityError, BadParameterException) as e:
                # fill in page with previously submitted values
                for field in mandatory_fields:
                    context.__setitem__(field, request.POST.get(field, "-- missing"))
                if isinstance(e, IntegrityError) and "username" in str(e):
                    context['alert'] = "Your chosen username is already used"
                else:
                    context['alert'] = str(e)
                return toaster_render(request, template, context)

        raise Exception("Invalid HTTP method for this page")

    # Shows the edit project page
    def project(request, pid):
        project = Project.objects.get(pk=pid)

        if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
            if request.GET:
                #Example:request.GET=<QueryDict: {'setMachine': ['qemuarm']}>
                params = urlencode(request.GET).replace('%5B%27','').replace('%27%5D','')
                return redirect("%s?%s" % (reverse(project_specific, args=(project.pk,)),params))
            else:
                return redirect(reverse(project_specific, args=(project.pk,)))
        context = {"project": project}
        return toaster_render(request, "project.html", context)

    # Shows the edit project-specific page
    def project_specific(request, pid):
        project = Project.objects.get(pk=pid)

        # Are we refreshing from a successful project specific update clone?
        if Project.PROJECT_SPECIFIC_CLONING_SUCCESS == project.get_variable(Project.PROJECT_SPECIFIC_STATUS):
            return redirect(reverse(landing_specific,args=(project.pk,)))

        context = {
            "project": project,
            "is_new" : project.get_variable(Project.PROJECT_SPECIFIC_ISNEW),
            "default_image_recipe" : project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE),
            "mru" : Build.objects.all().filter(project=project,outcome=Build.IN_PROGRESS),
            }
        if project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0:
            context['build_in_progress_none_completed'] = True
        else:
            context['build_in_progress_none_completed'] = False
        return toaster_render(request, "project.html", context)

    # perform the final actions for the project specific page
    def project_specific_finalize(cmnd, pid):
        project = Project.objects.get(pk=pid)
        callback = project.get_variable(Project.PROJECT_SPECIFIC_CALLBACK)
        if "update" == cmnd:
            # Delete all '_PROJECT_PREPARE_' builds
            for b in Build.objects.all().filter(project=project):
                delete_build = False
                for t in b.target_set.all():
                    if '_PROJECT_PREPARE_' == t.target:
                        delete_build = True
                if delete_build:
                    from django.core import management
                    management.call_command('builddelete', str(b.id), interactive=False)
            # perform callback at this last moment if defined, in case Toaster gets shutdown next
            default_target = project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE)
            if callback:
                callback = callback.replace("<IMAGE>",default_target)
        if "cancel" == cmnd:
            if callback:
                callback = callback.replace("<IMAGE>","none")
                callback = callback.replace("--update","--cancel")
        # perform callback at this last moment if defined, in case this Toaster gets shutdown next
        ret = ''
        if callback:
            ret = os.system('bash -c "%s"' % callback)
            project.set_variable(Project.PROJECT_SPECIFIC_CALLBACK,'')
        # Delete the temp project specific variables
        project.set_variable(Project.PROJECT_SPECIFIC_ISNEW,'')
        project.set_variable(Project.PROJECT_SPECIFIC_STATUS,Project.PROJECT_SPECIFIC_NONE)
        # WORKAROUND: Release this workaround flag
        project.set_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE','')

    # Shows the final landing page for project specific update
    def landing_specific(request, pid):
        project_specific_finalize("update", pid)
        context = {
            "install_dir": os.environ['TOASTER_DIR'],
        }
        return toaster_render(request, "landing_specific.html", context)

    # Shows the related landing-specific page
    def landing_specific_cancel(request, pid):
        project_specific_finalize("cancel", pid)
        context = {
            "install_dir": os.environ['TOASTER_DIR'],
            "status": "cancel",
        }
        return toaster_render(request, "landing_specific.html", context)

    def jsunittests(request):
        """ Provides a page for the js unit tests """
        bbv = BitbakeVersion.objects.filter(branch="master").first()
        release = Release.objects.filter(bitbake_version=bbv).first()

        name = "_js_unit_test_prj_"

        # If there is an existing project by this name delete it.
        # We don't want Lots of duplicates cluttering up the projects.
        Project.objects.filter(name=name).delete()

        new_project = Project.objects.create_project(name=name,
                                                     release=release)
        # Add a layer
        layer = new_project.get_all_compatible_layer_versions().first()

        ProjectLayer.objects.get_or_create(layercommit=layer,
                                           project=new_project)

        # make sure we have a machine set for this project
        ProjectVariable.objects.get_or_create(project=new_project,
                                              name="MACHINE",
                                              value="qemux86")
        context = {'project': new_project}
        return toaster_render(request, "js-unit-tests.html", context)

    from django.views.decorators.csrf import csrf_exempt
    @csrf_exempt
    def xhr_testreleasechange(request, pid):
        def response(data):
            return HttpResponse(jsonfilter(data),
                                content_type="application/json")

        """ returns layer versions that would be deleted on the new
        release__pk """
        try:
            prj = Project.objects.get(pk = pid)
            new_release_id = request.GET['new_release_id']

            # If we're already on this project do nothing
            if prj.release.pk == int(new_release_id):
                return reponse({"error": "ok", "rows": []})

            retval = []

            for project in prj.projectlayer_set.all():
                release = Release.objects.get(pk = new_release_id)

                layer_versions = prj.get_all_compatible_layer_versions()
                layer_versions = layer_versions.filter(release = release)
                layer_versions = layer_versions.filter(layer__name = project.layercommit.layer.name)

                # there is no layer_version with the new release id,
                # and the same name
                if layer_versions.count() < 1:
                    retval.append(project)

            return response({"error":"ok",
                             "rows": [_lv_to_dict(prj) for y in [x.layercommit for x in retval]]
                            })

        except Exception as e:
            return response({"error": str(e) })

    def xhr_configvaredit(request, pid):
        try:
            prj = Project.objects.get(id = pid)
            # There are cases where user can add variables which hold values
            # like http://, file:/// etc. In such case a simple split(":")
            # would fail. One example is SSTATE_MIRRORS variable. So we use
            # max_split var to handle them.
            max_split = 1
            # add conf variables
            if 'configvarAdd' in request.POST:
                t=request.POST['configvarAdd'].strip()
                if ":" in t:
                    variable, value = t.split(":", max_split)
                else:
                    variable = t
                    value = ""

                pt, created = ProjectVariable.objects.get_or_create(project = prj, name = variable, value = value)
            # change conf variables
            if 'configvarChange' in request.POST:
                t=request.POST['configvarChange'].strip()
                if ":" in t:
                    variable, value = t.split(":", max_split)
                else:
                    variable = t
                    value = ""

                pt, created = ProjectVariable.objects.get_or_create(project = prj, name = variable)
                pt.value=value
                pt.save()
            # remove conf variables
            if 'configvarDel' in request.POST:
                t=request.POST['configvarDel'].strip()
                pt = ProjectVariable.objects.get(pk = int(t)).delete()

            # return all project settings, filter out blacklist and elsewhere-managed variables
            vars_managed,vars_fstypes,vars_blacklist = get_project_configvars_context()
            configvars_query = ProjectVariable.objects.filter(project_id = pid).all()
            for var in vars_managed:
                configvars_query = configvars_query.exclude(name = var)
            for var in vars_blacklist:
                configvars_query = configvars_query.exclude(name = var)

            return_data = {
                "error": "ok",
                'configvars': [(x.name, x.value, x.pk) for x in configvars_query]
               }
            try:
                return_data['distro'] = ProjectVariable.objects.get(project = prj, name = "DISTRO").value,
            except ProjectVariable.DoesNotExist:
                pass
            try:
                return_data['dl_dir'] = ProjectVariable.objects.get(project = prj, name = "DL_DIR").value,
            except ProjectVariable.DoesNotExist:
                pass
            try:
                return_data['fstypes'] = ProjectVariable.objects.get(project = prj, name = "IMAGE_FSTYPES").value,
            except ProjectVariable.DoesNotExist:
                pass
            try:
                return_data['image_install_append'] = ProjectVariable.objects.get(project = prj, name = "IMAGE_INSTALL_append").value,
            except ProjectVariable.DoesNotExist:
                pass
            try:
                return_data['package_classes'] = ProjectVariable.objects.get(project = prj, name = "PACKAGE_CLASSES").value,
            except ProjectVariable.DoesNotExist:
                pass
            try:
                return_data['sstate_dir'] = ProjectVariable.objects.get(project = prj, name = "SSTATE_DIR").value,
            except ProjectVariable.DoesNotExist:
                pass

            return HttpResponse(json.dumps( return_data ), content_type = "application/json")

        except Exception as e:
            return HttpResponse(json.dumps({"error":str(e) + "\n" + traceback.format_exc()}), content_type = "application/json")


    def customrecipe_download(request, pid, recipe_id):
        recipe = get_object_or_404(CustomImageRecipe, pk=recipe_id)

        file_data = recipe.generate_recipe_file_contents()

        response = HttpResponse(file_data, content_type='text/plain')
        response['Content-Disposition'] = \
                'attachment; filename="%s_%s.bb"' % (recipe.name,
                                                     recipe.version)

        return response

    def importlayer(request, pid):
        template = "importlayer.html"
        context = {
            'project': Project.objects.get(id=pid),
        }
        return toaster_render(request, template, context)

    def layerdetails(request, pid, layerid):
        project = Project.objects.get(pk=pid)
        layer_version = Layer_Version.objects.get(pk=layerid)

        project_layers = ProjectLayer.objects.filter(
            project=project).values_list("layercommit_id",
                                         flat=True)

        context = {
            'project': project,
            'layer_source': LayerSource.types_dict(),
            'layerversion': layer_version,
            'layerdeps': {
                "list": [
                    {
                        "id": dep.id,
                        "name": dep.layer.name,
                        "layerdetailurl": reverse('layerdetails',
                                                  args=(pid, dep.pk)),
                        "vcs_url": dep.layer.vcs_url,
                        "vcs_reference": dep.get_vcs_reference()
                    }
                    for dep in layer_version.get_alldeps(project.id)]
            },
            'projectlayers': list(project_layers)
        }

        return toaster_render(request, 'layerdetails.html', context)


    def get_project_configvars_context():
        # Vars managed outside of this view
        vars_managed = {
            'MACHINE', 'BBLAYERS'
        }

        vars_blacklist  = {
            'PARALLEL_MAKE','BB_NUMBER_THREADS',
            'BB_DISKMON_DIRS','BB_NUMBER_THREADS','CVS_PROXY_HOST','CVS_PROXY_PORT',
            'PARALLEL_MAKE','TMPDIR',
            'all_proxy','ftp_proxy','http_proxy ','https_proxy'
            }

        vars_fstypes = Target_Image_File.SUFFIXES

        return(vars_managed,sorted(vars_fstypes),vars_blacklist)

    def projectconf(request, pid):

        try:
            prj = Project.objects.get(id = pid)
        except Project.DoesNotExist:
            return HttpResponseNotFound("<h1>Project id " + pid + " is unavailable</h1>")

        # remove blacklist and externally managed varaibles from this list
        vars_managed,vars_fstypes,vars_blacklist = get_project_configvars_context()
        configvars = ProjectVariable.objects.filter(project_id = pid).all()
        for var in vars_managed:
            configvars = configvars.exclude(name = var)
        for var in vars_blacklist:
            configvars = configvars.exclude(name = var)

        context = {
            'project':          prj,
            'configvars':       configvars,
            'vars_managed':     vars_managed,
            'vars_fstypes':     vars_fstypes,
            'vars_blacklist':   vars_blacklist,
        }

        try:
            context['distro'] =  ProjectVariable.objects.get(project = prj, name = "DISTRO").value
            context['distro_defined'] = "1"
        except ProjectVariable.DoesNotExist:
            pass
        try:
            if ProjectVariable.objects.get(project = prj, name = "DL_DIR").value == "${TOPDIR}/../downloads":
                be = BuildEnvironment.objects.get(pk = str(1))
                dl_dir = os.path.join(dirname(be.builddir), "downloads")
                context['dl_dir'] =  dl_dir
                pv, created = ProjectVariable.objects.get_or_create(project = prj, name = "DL_DIR")
                pv.value = dl_dir
                pv.save()
            else:
                context['dl_dir'] = ProjectVariable.objects.get(project = prj, name = "DL_DIR").value
            context['dl_dir_defined'] = "1"
        except (ProjectVariable.DoesNotExist, BuildEnvironment.DoesNotExist):
            pass
        try:
            context['fstypes'] =  ProjectVariable.objects.get(project = prj, name = "IMAGE_FSTYPES").value
            context['fstypes_defined'] = "1"
        except ProjectVariable.DoesNotExist:
            pass
        try:
            context['image_install_append'] =  ProjectVariable.objects.get(project = prj, name = "IMAGE_INSTALL_append").value
            context['image_install_append_defined'] = "1"
        except ProjectVariable.DoesNotExist:
            pass
        try:
            context['package_classes'] =  ProjectVariable.objects.get(project = prj, name = "PACKAGE_CLASSES").value
            context['package_classes_defined'] = "1"
        except ProjectVariable.DoesNotExist:
            pass
        try:
            if ProjectVariable.objects.get(project = prj, name = "SSTATE_DIR").value == "${TOPDIR}/../sstate-cache":
                be = BuildEnvironment.objects.get(pk = str(1))
                sstate_dir = os.path.join(dirname(be.builddir), "sstate-cache")
                context['sstate_dir'] = sstate_dir
                pv, created = ProjectVariable.objects.get_or_create(project = prj, name = "SSTATE_DIR")
                pv.value = sstate_dir
                pv.save()
            else:
                context['sstate_dir'] = ProjectVariable.objects.get(project = prj, name = "SSTATE_DIR").value
            context['sstate_dir_defined'] = "1"
        except (ProjectVariable.DoesNotExist, BuildEnvironment.DoesNotExist):
            pass

        return toaster_render(request, "projectconf.html", context)

    def _file_names_for_artifact(build, artifact_type, artifact_id):
        """
        Return a tuple (file path, file name for the download response) for an
        artifact of type artifact_type with ID artifact_id for build; if
        artifact type is not supported, returns (None, None)
        """
        file_name = None
        response_file_name = None

        if artifact_type == "cookerlog":
            file_name = build.cooker_log_path
            response_file_name = "cooker.log"

        elif artifact_type == "imagefile":
            file_name = Target_Image_File.objects.get(target__build = build, pk = artifact_id).file_name

        elif artifact_type == "targetkernelartifact":
            target = TargetKernelFile.objects.get(pk=artifact_id)
            file_name = target.file_name

        elif artifact_type == "targetsdkartifact":
            target = TargetSDKFile.objects.get(pk=artifact_id)
            file_name = target.file_name

        elif artifact_type == "licensemanifest":
            file_name = Target.objects.get(build = build, pk = artifact_id).license_manifest_path

        elif artifact_type == "packagemanifest":
            file_name = Target.objects.get(build = build, pk = artifact_id).package_manifest_path

        elif artifact_type == "tasklogfile":
            file_name = Task.objects.get(build = build, pk = artifact_id).logfile

        elif artifact_type == "logmessagefile":
            file_name = LogMessage.objects.get(build = build, pk = artifact_id).pathname

        if file_name and not response_file_name:
            response_file_name = os.path.basename(file_name)

        return (file_name, response_file_name)

    def build_artifact(request, build_id, artifact_type, artifact_id):
        """
        View which returns a build artifact file as a response
        """
        file_name = None
        response_file_name = None

        try:
            build = Build.objects.get(pk = build_id)
            file_name, response_file_name = _file_names_for_artifact(
                build, artifact_type, artifact_id
            )

            if file_name and response_file_name:
                fsock = open(file_name, "rb")
                content_type = MimeTypeFinder.get_mimetype(file_name)

                response = HttpResponse(fsock, content_type = content_type)

                disposition = "attachment; filename=" + response_file_name
                response["Content-Disposition"] = disposition

                return response
            else:
                return toaster_render(request, "unavailable_artifact.html")
        except (ObjectDoesNotExist, IOError):
            return toaster_render(request, "unavailable_artifact.html")

