bitbake: toaster: build date range selections

Enable date range selections for build start and build complete in all
builds page for both managed and interactive mode. Disable the filter
counts.

[YOCTO #6040]
[YOCTO #7249]
[YOCTO #7461]

(Bitbake rev: 7c86ed5fb51c6237fa40fb454e58564ef027dd51)

Signed-off-by: David Reyna <David.Reyna@windriver.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
David Reyna 2015-03-21 18:01:38 -07:00 committed by Richard Purdie
parent e840b7a8fb
commit 6a934f488f
6 changed files with 277 additions and 24 deletions

View File

@ -0,0 +1,95 @@
"use strict"
// The disable removes the 'datepicker' attribute and
// settings, so you have to re-initialize it each time
// the date range is selected and enabled
// DOM is used instead of jQuery to find the elements
// in all contexts
function date_enable (key, action) {
var elemFrom=document.getElementById("date_from_"+key);
var elemTo=document.getElementById("date_to_"+key);
if ('enable' == action) {
elemFrom.removeAttribute("disabled");
elemTo.removeAttribute("disabled");
$(elemFrom).datepicker();
$(elemTo).datepicker();
$(elemFrom).datepicker( "option", "dateFormat", "dd/mm/yy" );
$(elemTo).datepicker( "option", "dateFormat", "dd/mm/yy" );
$(elemFrom).datepicker( "setDate", elemFrom.getAttribute( "data-setDate") );
$(elemTo).datepicker( "setDate", elemTo.getAttribute( "data-setDate") );
$(elemFrom).datepicker( "option", "minDate", elemFrom.getAttribute( "data-minDate"));
$(elemTo).datepicker( "option", "minDate", elemTo.getAttribute( "data-minDate"));
$(elemFrom).datepicker( "option", "maxDate", elemFrom.getAttribute( "data-maxDate"));
$(elemTo).datepicker( "option", "maxDate", elemTo.getAttribute( "data-maxDate"));
} else {
elemFrom.setAttribute("disabled","disabled");
elemTo.setAttribute("disabled","disabled");
}
}
// Initialize the date picker elements with their default state variables, and
// register the radio button and form actions
function date_init (key, from_date, to_date, min_date, max_date, initial_enable) {
var elemFrom=document.getElementById("date_from_"+key);
var elemTo=document.getElementById("date_to_"+key);
// Were there any daterange filters instantiated? (e.g. no builds found)
if (null == elemFrom) {
return;
}
// init the datepicker context data
elemFrom.setAttribute( "data-setDate", from_date );
elemTo.setAttribute( "data-setDate", to_date );
elemFrom.setAttribute( "data-minDate", min_date);
elemTo.setAttribute( "data-minDate", min_date);
elemFrom.setAttribute( "data-maxDate", max_date);
elemTo.setAttribute( "data-maxDate", max_date);
// does the date set start enabled?
if (key == initial_enable) {
date_enable (key, "enable");
} else {
date_enable (key, "disable");
}
// catch the radio button selects for enable/disable
$('input:radio[name="filter"]').change(function(){
if ($(this).val() == 'daterange') {
key=$(this).attr("data-key");
date_enable (key, 'enable');
} else {
key=$(this).attr("data-key");
date_enable (key, 'disable');
}
});
// catch any new 'from' date as minDate for 'to' date
$("#date_from_"+key).change(function(){
from_date = $("#date_from_"+key).val();
$("#date_to_"+key).datepicker( "option", "minDate", from_date );
});
// catch the submit (just once)
$("form").unbind('submit');
$("form").submit(function(e) {
// format a composite daterange filter value so that it can be parsed and post-processed in the view
var key=e.originalEvent.explicitOriginalTarget.getAttribute("data-key")
if (typeof key != "undefined") {
if ($("#date_from_"+key).length) {
var filter=key+"__gte!"+key+"__lt:"+$("#date_from_"+key).val()+"!"+$("#date_to_"+key).val()+"_daterange";
$("#last_date_from_"+key).val($("#date_from_"+key).val());
$("#last_date_to_"+key).val($("#date_to_"+key).val());
$("#filter_value_"+key).val(filter);
}
}
return true;
});
};

View File

@ -4,7 +4,24 @@
{% load projecttags %}
{% load humanize %}
{% block extraheadcontent %}
<link rel="stylesheet" href="/static/css/jquery-ui.min.css" type='text/css'>
<link rel="stylesheet" href="/static/css/jquery-ui.structure.min.css" type='text/css'>
<link rel="stylesheet" href="/static/css/jquery-ui.theme.min.css" type='text/css'>
<script src="/static/js/jquery-ui.min.js"></script>
<script src="/static/js/filtersnippet.js"></script>
{% endblock %}
{% block pagecontent %}
<script>
// intiialize the date range controls
$(document).ready(function () {
date_init('started_on','{{last_date_from}}','{{last_date_to}}','{{dateMin_started_on}}','{{dateMax_started_on}}','{{daterange_selected}}');
date_init('completed_on','{{last_date_from}}','{{last_date_to}}','{{dateMin_completed_on}}','{{dateMax_completed_on}}','{{daterange_selected}}');
});
</script>
<div class="row-fluid">
{% include "mrb_section.html" %}

View File

@ -1,5 +1,6 @@
{% load projecttags %}
<!-- '{{f.class}}' filter -->
{% with f.class as key %}
<form id="filter_{{f.class}}" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
<input type="hidden" name="search" value="{{request.GET.search}}"/>
<div class="modal-header">
@ -13,22 +14,37 @@
<div class="modal-body">
<p>{{f.label}}</p>
<label class="radio">
<input type="radio" name="filter" {%if request.GET.filter%}{{f.options|check_filter_status:request.GET.filter}} {%else%} checked {%endif%} value=""> All {%if filter_search_display%}{{filter_search_display|title}}{%else%}{{objectname|title}}{%endif%} ({{total_count}})
<input type="radio" name="filter" {%if request.GET.filter%}{{f.options|check_filter_status:request.GET.filter}} {%else%} checked {%endif%} value="" data-key="{{key}}"> All {%if filter_search_display%}{{filter_search_display|title}}{%else%}{{objectname|title}}{%endif%}
</label>
{% for option in f.options %}
{% if option.2 %}
<label class="radio">
<input type="radio" name="filter" {%if request.GET.filter == option.1 %}checked{%endif%} value="{{option.1}}"> {{option.0}} (<span id="{{option.1}}_count">{{option.2}}</span>)
{% if option.1 == 'daterange' %}
<div class="form-inline">
<label class="radio">
<input type="radio" name="filter" id="filter_value_{{key}}" {%if key == daterange_selected %}checked{%endif%} value="{{option.1}}" data-key="{{key}}"> {{option.0}}
{% else %}
<label class="radio muted">
<input type="radio" name="filter" disabled {%if request.GET.filter == option.1 %}checked{%endif%} value="{{option.1}}"> {{option.0}} (<span id="{{option.1}}_count">{{option.2}}</span>)
{% if 1 %}
<label class="radio">
<input type="radio" name="filter" {%if request.GET.filter == option.1 %}checked{%endif%} value="{{option.1}}" data-key="{{key}}"> {{option.0}}
{% comment "do not disable radio selections by count for now" %}{% else %}
<label class="radio muted">
<input type="radio" name="filter" disabled {%if request.GET.filter == option.1 %}checked{%endif%} value="{{option.1}}" data-key="{{key}}"> {{option.0}}
{% endcomment %}{% endif %}
{% endif %}
{% if option.3 %}<i class="icon-question-sign get-help" data-placement="right" title="{{option.3}}"></i>{% endif %}
</label>
</label>
{% if option.1 == 'daterange' %}
<input type="text" id="date_from_{{key}}" name="date_from_{{key}}" disabled class="input-small" /><label class="help-inline">to</label>
<input type="text" id="date_to_{{key}}" name="date_to_{{key}}" disabled class="input-small" />
<label class="help-inline get-help" >(dd/mm/yyyy)</label>
</div>
{% endif %}
{% endfor %}
<!-- daterange persistence -->
<input type="hidden" id="last_date_from_{{key}}" name="last_date_from" value="{{last_date_from}}"/>
<input type="hidden" id="last_date_to_{{key}}" name="last_date_to" value="{{last_date_to}}"/>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Apply</button>
<button type="submit" class="btn btn-primary" data-key="{{key}}">Apply</button>
{% if request.GET.filter %}
{% if request.GET.filter|string_remove_regex:':.*' != f.options.0.1|string_remove_regex:':.*' %}
<span class="help-inline pull-left">You can only apply one filter to the table. This filter will override the current filter.</span>
@ -36,4 +52,4 @@
{% endif %}
</div>
</form>
{% endwith %}

View File

@ -4,7 +4,24 @@
{% load projecttags %}
{% load humanize %}
{% block extraheadcontent %}
<link rel="stylesheet" href="/static/css/jquery-ui.min.css" type='text/css'>
<link rel="stylesheet" href="/static/css/jquery-ui.structure.min.css" type='text/css'>
<link rel="stylesheet" href="/static/css/jquery-ui.theme.min.css" type='text/css'>
<script src="/static/js/jquery-ui.min.js"></script>
<script src="/static/js/filtersnippet.js"></script>
{% endblock %}
{% block pagecontent %}
<script>
// initialize the date range controls
$(document).ready(function () {
date_init('created','{{last_date_from}}','{{last_date_to}}','{{dateMin_created}}','{{dateMax_created}}','{{daterange_selected}}');
date_init('updated','{{last_date_from}}','{{last_date_to}}','{{dateMin_updated}}','{{dateMax_updated}}','{{daterange_selected}}');
});
</script>
<div class="row-fluid">
{% include "managed_mrb_section.html" %}
@ -47,7 +64,7 @@
{% for buildrequest in objects %}{% if buildrequest.build %} {% with build=buildrequest.build %} {# if we have a build, just display it #}
<tr class="data">
<td class="outcome"><a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a>
{% if build.project %}
{% if build.project %}
&nbsp; <a href="{% url 'build_artifact' build.id "cookerlog" build.id %}">
<i class="icon-download-alt" title="" data-original-title="Download build log"></i>
</a>

View File

@ -125,6 +125,8 @@ def filtered_icon(options, filter):
for option in options:
if filter == option[1]:
return "btn-primary"
if ('daterange' == option[1]) and filter.startswith(option[4]):
return "btn-primary"
return ""
@register.filter
@ -134,6 +136,8 @@ def filtered_tooltip(options, filter):
for option in options:
if filter == option[1]:
return "Showing only %s"%option[0]
if ('daterange' == option[1]) and filter.startswith(option[4]):
return "Showing only %s"%option[0]
return ""
@register.filter

View File

@ -35,7 +35,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.utils import timezone
from django.utils.html import escape
from datetime import timedelta
from datetime import timedelta, datetime, date
from django.utils import formats
from toastergui.templatetags.projecttags import json as jsonfilter
import json
@ -279,6 +279,65 @@ def _save_parameters_cookies(response, pagesize, orderby, request):
response.set_cookie(key='orderby', value=html_parser.unescape(orderby), path=request.path)
return response
# 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())
today_begin = date(today_begin.year,today_begin.month,today_begin.day)
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)
context_date['dateMin_'+key]=timezone.localtime(getattr(queryset_key.first(),key)).strftime("%d/%m/%Y")
context_date['dateMax_'+key]=timezone.localtime(getattr(queryset_key.last(),key)).strftime("%d/%m/%Y")
return context_date,today_begin,yesterday_begin
##
# build dashboard for a single build, coming in as argument
@ -1868,6 +1927,9 @@ if toastermain.settings.MANAGED:
# boilerplate code that takes a request for an object type and returns a queryset
# for that object type. copypasta for all needed table searches
(filter_string, search_term, ordering_string) = _search_tuple(request, BuildRequest)
# post-process any date range filters
filter_string,daterange_selected = _modify_date_range_filter(filter_string)
# we don't display in-progress or deleted builds
queryset_all = buildrequests.exclude(state = BuildRequest.REQ_DELETED)
queryset_all = queryset_all.select_related("build", "build__project").annotate(Count('brerror'))
@ -1918,6 +1980,7 @@ if toastermain.settings.MANAGED:
'fstypes' : fstypes_map,
'search_term' : search_term,
'total_count' : queryset_with_search.count(),
'daterange_selected' : daterange_selected,
# Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
'tablecols' : [
{'name': 'Outcome', # column with a single filter
@ -1956,6 +2019,10 @@ if toastermain.settings.MANAGED:
}
)
# calculate the exact begining of local today and yesterday
context_date,today_begin,yesterday_begin = _add_daterange_context(queryset_all, request, {'created','updated'})
context.update(context_date)
context['tablecols'].append(
{'name': 'Started on', 'clclass': 'started_on', 'hidden' : 1, # this is an unchecked box, which hides the column
'qhelp': "The date and time you started the build",
@ -1964,9 +2031,16 @@ if toastermain.settings.MANAGED:
'filter' : {'class' : 'created',
'label': 'Show:',
'options' : [
("Today's builds" , 'created__gte:'+timezone.now().strftime("%Y-%m-%d"), queryset_all.filter(created__gte=timezone.now()).count()),
("Yesterday's builds", 'created__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_all.filter(created__gte=(timezone.now()-timedelta(hours=24))).count()),
("This week's builds", 'created__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_all.filter(created__gte=(timezone.now()-timedelta(days=7))).count()),
("Today's builds" , 'created__gte:'+today_begin.strftime("%Y-%m-%d"), queryset_all.filter(created__gte=today_begin).count()),
("Yesterday's builds",
'created__gte!created__lt:'
+yesterday_begin.strftime("%Y-%m-%d")+'!'
+today_begin.strftime("%Y-%m-%d"),
queryset_all.filter(
created__gte=yesterday_begin,
created__lt=today_begin
).count()),
("Build date range", 'daterange', 1, '', 'created'),
]
}
}
@ -1980,9 +2054,16 @@ if toastermain.settings.MANAGED:
'filter' : {'class' : 'updated',
'label': 'Show:',
'options' : [
("Today's builds", 'updated__gte:'+timezone.now().strftime("%Y-%m-%d"), queryset_all.filter(updated__gte=timezone.now()).count()),
("Yesterday's builds", 'updated__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_all.filter(updated__gte=(timezone.now()-timedelta(hours=24))).count()),
("This week's builds", 'updated__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_all.filter(updated__gte=(timezone.now()-timedelta(days=7))).count()),
("Today's builds" , 'updated__gte:'+today_begin.strftime("%Y-%m-%d"), queryset_all.filter(updated__gte=today_begin).count()),
("Yesterday's builds",
'updated__gte!updated__lt:'
+yesterday_begin.strftime("%Y-%m-%d")+'!'
+today_begin.strftime("%Y-%m-%d"),
queryset_all.filter(
updated__gte=yesterday_begin,
updated__lt=today_begin
).count()),
("Build date range", 'daterange', 1, '', 'updated'),
]
}
}
@ -3280,6 +3361,8 @@ else:
# boilerplate code that takes a request for an object type and returns a queryset
# for that object type. copypasta for all needed table searches
(filter_string, search_term, ordering_string) = _search_tuple(request, Build)
# post-process any date range filters
filter_string,daterange_selected = _modify_date_range_filter(filter_string)
queryset_all = Build.objects.exclude(outcome = Build.IN_PROGRESS)
queryset_with_search = _get_queryset(Build, queryset_all, None, search_term, ordering_string, '-completed_on')
queryset = _get_queryset(Build, queryset_all, filter_string, search_term, ordering_string, '-completed_on')
@ -3290,6 +3373,9 @@ else:
# build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds)
build_mru = Build.objects.order_by("-started_on")[:3]
# calculate the exact begining of local today and yesterday, append context
context_date,today_begin,yesterday_begin = _add_daterange_context(queryset_all, request, {'started_on','completed_on'})
# set up list of fstypes for each build
fstypes_map = {};
for build in build_info:
@ -3320,6 +3406,7 @@ else:
'fstypes' : fstypes_map,
'search_term' : search_term,
'total_count' : queryset_with_search.count(),
'daterange_selected' : daterange_selected,
# Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
'tablecols' : [
{'name': 'Outcome', # column with a single filter
@ -3356,12 +3443,19 @@ else:
'filter' : {'class' : 'started_on',
'label': 'Show:',
'options' : [
("Today's builds" , 'started_on__gte:'+timezone.now().strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=timezone.now()).count()),
("Yesterday's builds", 'started_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=(timezone.now()-timedelta(hours=24))).count()),
("This week's builds", 'started_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_with_search.filter(started_on__gte=(timezone.now()-timedelta(days=7))).count()),
("Today's builds" , 'started_on__gte:'+today_begin.strftime("%Y-%m-%d"), queryset_all.filter(started_on__gte=today_begin).count()),
("Yesterday's builds",
'started_on__gte!started_on__lt:'
+yesterday_begin.strftime("%Y-%m-%d")+'!'
+today_begin.strftime("%Y-%m-%d"),
queryset_all.filter(
started_on__gte=yesterday_begin,
started_on__lt=today_begin
).count()),
("Build date range", 'daterange', 1, '', 'started_on'),
]
}
},
},
{'name': 'Completed on',
'qhelp': "The date and time the build finished",
'orderfield': _get_toggle_order(request, "completed_on", True),
@ -3370,9 +3464,16 @@ else:
'filter' : {'class' : 'completed_on',
'label': 'Show:',
'options' : [
("Today's builds", 'completed_on__gte:'+timezone.now().strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=timezone.now()).count()),
("Yesterday's builds", 'completed_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=(timezone.now()-timedelta(hours=24))).count()),
("This week's builds", 'completed_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), queryset_with_search.filter(completed_on__gte=(timezone.now()-timedelta(days=7))).count()),
("Today's builds" , 'completed_on__gte:'+today_begin.strftime("%Y-%m-%d"), queryset_all.filter(completed_on__gte=today_begin).count()),
("Yesterday's builds",
'completed_on__gte!completed_on__lt:'
+yesterday_begin.strftime("%Y-%m-%d")+'!'
+today_begin.strftime("%Y-%m-%d"),
queryset_all.filter(
completed_on__gte=yesterday_begin,
completed_on__lt=today_begin
).count()),
("Build date range", 'daterange', 1, '', 'completed_on'),
]
}
},
@ -3433,6 +3534,9 @@ else:
]
}
# merge daterange values
context.update(context_date)
response = render(request, template, context)
_save_parameters_cookies(response, pagesize, orderby, request)
return response