bitbake: toastergui: implement date range filters for builds

Implement the completed_on and started_on filtering for
builds.

Also separate the name of a filter ("filter" in the querystring)
from its value ("filter_value" in the querystring). This enables
filtering to be defined in the querystring more intuitively,
and also makes it easier to add other types of filter (e.g.
by day).

[YOCTO #8738]

(Bitbake rev: d47c32e88c2d4a423f4d94d49759e557f425a539)

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
Signed-off-by: Ed Bartosh <ed.bartosh@linux.intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
Elliot Smith 2016-01-15 13:00:53 +02:00 committed by Richard Purdie
parent b929889cdd
commit f8d383d87f
6 changed files with 332 additions and 86 deletions

View File

@ -2,10 +2,11 @@ class QuerysetFilter(object):
""" Filter for a queryset """
def __init__(self, criteria=None):
self.criteria = None
if criteria:
self.set_criteria(criteria)
def set_criteria(self, criteria = None):
def set_criteria(self, criteria):
"""
criteria is an instance of django.db.models.Q;
see https://docs.djangoproject.com/en/1.9/ref/models/querysets/#q-objects

View File

@ -397,11 +397,140 @@ function tableInit(ctx){
$.cookie("cols", JSON.stringify(disabled_cols));
}
/**
* Create the DOM/JS for the client side of a TableFilterActionToggle
*
* filterName: (string) internal name for the filter action
* filterActionData: (object)
* filterActionData.count: (number) The number of items this filter will
* show when selected
*/
function createActionToggle(filterName, filterActionData) {
var actionStr = '<div class="radio">' +
'<input type="radio" name="filter"' +
' value="' + filterName + '"';
if (Number(filterActionData.count) == 0) {
actionStr += ' disabled="disabled"';
}
actionStr += ' id="' + filterName + '">' +
'<input type="hidden" name="filter_value" value="on"' +
' data-value-for="' + filterName + '">' +
'<label class="filter-title"' +
' for="' + filterName + '">' +
filterActionData.title +
' (' + filterActionData.count + ')' +
'</label>' +
'</div>';
return $(actionStr);
}
/**
* Create the DOM/JS for the client side of a TableFilterActionDateRange
*
* filterName: (string) internal name for the filter action
* filterValue: (string) from,to date range in format yyyy-mm-dd,yyyy-mm-dd;
* used to select the current values for the from/to datepickers;
* if this is partial (e.g. "yyyy-mm-dd,") only the applicable datepicker
* will have a date pre-selected; if empty, neither will
* filterActionData: (object) data for generating the action's HTML
* filterActionData.title: label for the radio button
* filterActionData.max: (string) maximum date for the pickers, in ISO 8601
* datetime format
* filterActionData.min: (string) minimum date for the pickers, ISO 8601
* datetime
*/
function createActionDateRange(filterName, filterValue, filterActionData) {
var action = $('<div class="radio">' +
'<input type="radio" name="filter"' +
' value="' + filterName + '" ' +
' id="' + filterName + '">' +
'<input type="hidden" name="filter_value" value=""' +
' data-value-for="' + filterName + '">' +
'<label class="filter-title"' +
' for="' + filterName + '">' +
filterActionData.title +
'</label>' +
'<input type="text" maxlength="10" class="input-small"' +
' data-date-from-for="' + filterName + '">' +
'<span class="help-inline">to</span>' +
'<input type="text" maxlength="10" class="input-small"' +
' data-date-to-for="' + filterName + '">' +
'<span class="help-inline get-help">(yyyy-mm-dd)</span>' +
'</div>');
var radio = action.find('[type="radio"]');
var value = action.find('[data-value-for]');
// make the datepickers for the range
var options = {
dateFormat: 'yy-mm-dd',
maxDate: new Date(filterActionData.max),
minDate: new Date(filterActionData.min)
};
// create date pickers, setting currently-selected from and to
// dates
var selectedFrom = null;
var selectedTo = null;
var selectedFromAndTo = [];
if (filterValue) {
selectedFromAndTo = filterValue.split(',');
}
if (selectedFromAndTo.length == 2) {
selectedFrom = selectedFromAndTo[0];
selectedTo = selectedFromAndTo[1];
}
options.defaultDate = selectedFrom;
var inputFrom =
action.find('[data-date-from-for]').datepicker(options);
inputFrom.val(selectedFrom);
options.defaultDate = selectedTo;
var inputTo =
action.find('[data-date-to-for]').datepicker(options);
inputTo.val(selectedTo);
// set filter_value based on date pickers when
// one of their values changes
var changeHandler = function () {
value.val(inputFrom.val() + ',' + inputTo.val());
};
inputFrom.change(changeHandler);
inputTo.change(changeHandler);
// check the associated radio button on clicking a date picker
var checkRadio = function () {
radio.prop('checked', 'checked');
};
inputFrom.focus(checkRadio);
inputTo.focus(checkRadio);
// selecting a date in a picker constrains the date you can
// set in the other picker
inputFrom.change(function () {
inputTo.datepicker('option', 'minDate', inputFrom.val());
});
inputTo.change(function () {
inputFrom.datepicker('option', 'maxDate', inputTo.val());
});
return action;
}
function filterOpenClicked(){
var filterName = $(this).data('filter-name');
/* We need to pass in the curren search so that the filter counts take
* into account the current search filter
/* We need to pass in the current search so that the filter counts take
* into account the current search term
*/
var params = {
'name' : filterName,
@ -443,46 +572,44 @@ function tableInit(ctx){
when the filter popup's "Apply" button is clicked, the
value for the radio button which is checked is passed in the
querystring and applied to the queryset on the table
*/
*/
var filterActionRadios = $('#filter-actions-' + ctx.tableName);
var filterActionRadios = $('#filter-actions-'+ctx.tableName);
$('#filter-modal-title-' + ctx.tableName).text(filterData.title);
$('#filter-modal-title-'+ctx.tableName).text(filterData.title);
filterActionRadios.text("");
filterActionRadios.empty();
// create a radio button + form elements for each action associated
// with the filter on this column of the table
for (var i in filterData.filter_actions) {
var filterAction = filterData.filter_actions[i];
var action = null;
var filterActionData = filterData.filter_actions[i];
var filterName = filterData.name + ':' +
filterActionData.action_name;
if (filterAction.type === 'toggle') {
var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
if (filterActionData.type === 'toggle') {
action = createActionToggle(filterName, filterActionData);
}
else if (filterActionData.type === 'daterange') {
var filterValue = tableParams.filter_value;
action = $('<label class="radio">' +
'<input type="radio" name="filter" value="">' +
'<span class="filter-title">' +
actionTitle +
'</span>' +
'</label>');
var radioInput = action.children("input");
if (Number(filterAction.count) == 0) {
radioInput.attr("disabled", "disabled");
}
radioInput.val(filterData.name + ':' + filterAction.action_name);
/* Setup the current selected filter, default to 'all' if
* no current filter selected.
*/
if ((tableParams.filter &&
tableParams.filter === radioInput.val()) ||
filterAction.action_name == 'all') {
radioInput.attr("checked", "checked");
}
action = createActionDateRange(
filterName,
filterValue,
filterActionData
);
}
if (action) {
// Setup the current selected filter, default to 'all' if
// no current filter selected
var radioInput = action.children('input[name="filter"]');
if ((tableParams.filter &&
tableParams.filter === radioInput.val()) ||
filterActionData.action_name == 'all') {
radioInput.attr("checked", "checked");
}
filterActionRadios.append(action);
}
}
@ -571,7 +698,14 @@ function tableInit(ctx){
filterBtnActive($(filterBtn), false);
});
tableParams.filter = $(this).find("input[type='radio']:checked").val();
// checked radio button
var checkedFilter = $(this).find("input[name='filter']:checked");
tableParams.filter = checkedFilter.val();
// hidden field holding the value for the checked filter
var checkedFilterValue = $(this).find("input[data-value-for='" +
tableParams.filter + "']");
tableParams.filter_value = checkedFilterValue.val();
var filterBtn = $("#" + tableParams.filter.split(":")[0]);

View File

@ -18,12 +18,15 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from django.db.models import Q, Max, Min
from django.utils import dateparse, timezone
class TableFilter(object):
"""
Stores a filter for a named field, and can retrieve the action
requested for that filter
requested from the set of actions for that filter
"""
def __init__(self, name, title):
self.name = name
self.title = title
@ -64,42 +67,128 @@ class TableFilter(object):
'filter_actions': filter_actions
}
class TableFilterActionToggle(object):
class TableFilterAction(object):
"""
Stores a single filter action which will populate one radio button of
a ToasterTable filter popup; this filter can either be on or off and
has no other parameters
A filter action which displays in the filter popup for a ToasterTable
and uses an associated QuerysetFilter to filter the queryset for that
ToasterTable
"""
def __init__(self, name, title, queryset_filter):
self.name = name
self.title = title
self.__queryset_filter = queryset_filter
self.type = 'toggle'
self.queryset_filter = queryset_filter
def set_params(self, params):
# set in subclasses
self.type = None
def set_filter_params(self, params):
"""
params: (str) a string of extra parameters for the action;
the structure of this string depends on the type of action;
it's ignored for a toggle filter action, which is just on or off
"""
pass
if not params:
return
def filter(self, queryset):
return self.__queryset_filter.filter(queryset)
return self.queryset_filter.filter(queryset)
def to_json(self, queryset):
""" Dump as a JSON object """
return {
'title': self.title,
'type': self.type,
'count': self.__queryset_filter.count(queryset)
'count': self.queryset_filter.count(queryset)
}
class TableFilterActionToggle(TableFilterAction):
"""
A single filter action which will populate one radio button of
a ToasterTable filter popup; this filter can either be on or off and
has no other parameters
"""
def __init__(self, *args):
super(TableFilterActionToggle, self).__init__(*args)
self.type = 'toggle'
class TableFilterActionDateRange(TableFilterAction):
"""
A filter action which will filter the queryset by a date range.
The date range can be set via set_params()
"""
def __init__(self, name, title, field, queryset_filter):
"""
field: the field to find the max/min range from in the queryset
"""
super(TableFilterActionDateRange, self).__init__(
name,
title,
queryset_filter
)
self.type = 'daterange'
self.field = field
def set_filter_params(self, params):
"""
params: (str) a string of extra parameters for the filtering
in the format "2015-12-09,2015-12-11" (from,to); this is passed in the
querystring and used to set the criteria on the QuerysetFilter
associated with this action
"""
# if params are invalid, return immediately, resetting criteria
# on the QuerysetFilter
try:
from_date_str, to_date_str = params.split(',')
except ValueError:
self.queryset_filter.set_criteria(None)
return
# one of the values required for the filter is missing, so set
# it to the one which was supplied
if from_date_str == '':
from_date_str = to_date_str
elif to_date_str == '':
to_date_str = from_date_str
date_from_naive = dateparse.parse_datetime(from_date_str + ' 00:00:00')
date_to_naive = dateparse.parse_datetime(to_date_str + ' 23:59:59')
tz = timezone.get_default_timezone()
date_from = timezone.make_aware(date_from_naive, tz)
date_to = timezone.make_aware(date_to_naive, tz)
args = {}
args[self.field + '__gte'] = date_from
args[self.field + '__lte'] = date_to
criteria = Q(**args)
self.queryset_filter.set_criteria(criteria)
def to_json(self, queryset):
""" Dump as a JSON object """
data = super(TableFilterActionDateRange, self).to_json(queryset)
# additional data about the date range covered by the queryset's
# records, retrieved from its <field> column
data['min'] = queryset.aggregate(Min(self.field))[self.field + '__min']
data['max'] = queryset.aggregate(Max(self.field))[self.field + '__max']
# a range filter has a count of None, as the number of records it
# will select depends on the date range entered
data['count'] = None
return data
class TableFilterMap(object):
"""
Map from field names to Filter objects for those fields
Map from field names to TableFilter objects for those fields
"""
def __init__(self):
self.__filters = {}

View File

@ -29,7 +29,9 @@ from django.core.urlresolvers import reverse
from django.views.generic import TemplateView
import itertools
from toastergui.tablefilter import TableFilter, TableFilterActionToggle
from toastergui.tablefilter import TableFilter
from toastergui.tablefilter import TableFilterActionToggle
from toastergui.tablefilter import TableFilterActionDateRange
class ProjectFilters(object):
def __init__(self, project_layers):
@ -1070,6 +1072,7 @@ class BuildsTable(ToasterTable):
help_text='The date and time when the build started',
hideable=True,
orderable=True,
filter_name='started_on_filter',
static_data_name='started_on',
static_data_template=started_on_template)
@ -1077,6 +1080,7 @@ class BuildsTable(ToasterTable):
help_text='The date and time when the build finished',
hideable=False,
orderable=True,
filter_name='completed_on_filter',
static_data_name='completed_on',
static_data_template=completed_on_template)
@ -1149,6 +1153,38 @@ class BuildsTable(ToasterTable):
outcome_filter.add_action(failed_builds_filter_action)
self.add_filter(outcome_filter)
# started on
started_on_filter = TableFilter(
'started_on_filter',
'Filter by date when build was started'
)
by_started_date_range_filter_action = TableFilterActionDateRange(
'date_range',
'Build date range',
'started_on',
QuerysetFilter()
)
started_on_filter.add_action(by_started_date_range_filter_action)
self.add_filter(started_on_filter)
# completed on
completed_on_filter = TableFilter(
'completed_on_filter',
'Filter by date when build was completed'
)
by_completed_date_range_filter_action = TableFilterActionDateRange(
'date_range',
'Build date range',
'completed_on',
QuerysetFilter()
)
completed_on_filter.add_action(by_completed_date_range_filter_action)
self.add_filter(completed_on_filter)
# failed tasks
failed_tasks_filter = TableFilter(
'failed_tasks_filter',

View File

@ -1,4 +1,13 @@
{% extends 'base.html' %}
{% load static %}
{% 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>
{% endblock %}
{% block title %} All builds - Toaster {% endblock %}
@ -34,29 +43,6 @@
titleElt.text(title);
});
/* {% if last_date_from and last_date_to %}
// TODO initialize the date range controls;
// this will need to be added via ToasterTable
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}}"
);
{% endif %}
*/
});
</script>
{% endblock %}

View File

@ -183,13 +183,13 @@ class ToasterTable(TemplateView):
return template.render(context)
def apply_filter(self, filters, **kwargs):
def apply_filter(self, filters, filter_value, **kwargs):
"""
Apply a filter submitted in the querystring to the ToasterTable
filters: (str) in the format:
'<filter name>:<action name>!<action params>'
where <action params> is optional
'<filter name>:<action name>'
filter_value: (str) parameters to pass to the named filter
<filter name> and <action name> are used to look up the correct filter
in the ToasterTable's filter map; the <action params> are set on
@ -199,15 +199,8 @@ class ToasterTable(TemplateView):
self.setup_filters(**kwargs)
try:
filter_name, action_name_and_params = filters.split(':')
action_name = None
action_params = None
if re.search('!', action_name_and_params):
action_name, action_params = action_name_and_params.split('!')
action_params = urllib.unquote_plus(action_params)
else:
action_name = action_name_and_params
filter_name, action_name = filters.split(':')
action_params = urllib.unquote_plus(filter_value)
except ValueError:
return
@ -217,7 +210,7 @@ class ToasterTable(TemplateView):
try:
table_filter = self.filter_map.get_filter(filter_name)
action = table_filter.get_action(action_name)
action.set_params(action_params)
action.set_filter_params(action_params)
self.queryset = action.filter(self.queryset)
except KeyError:
# pass it to the user - programming error here
@ -247,13 +240,20 @@ class ToasterTable(TemplateView):
def get_data(self, request, **kwargs):
"""Returns the data for the page requested with the specified
parameters applied"""
"""
Returns the data for the page requested with the specified
parameters applied
filters: filter and action name, e.g. "outcome:build_succeeded"
filter_value: value to pass to the named filter+action, e.g. "on"
(for a toggle filter) or "2015-12-11,2015-12-12" (for a date range filter)
"""
page_num = request.GET.get("page", 1)
limit = request.GET.get("limit", 10)
search = request.GET.get("search", None)
filters = request.GET.get("filter", None)
filter_value = request.GET.get("filter_value", "on")
orderby = request.GET.get("orderby", None)
nocache = request.GET.get("nocache", None)
@ -285,7 +285,7 @@ class ToasterTable(TemplateView):
if search:
self.apply_search(search)
if filters:
self.apply_filter(filters, **kwargs)
self.apply_filter(filters, filter_value, **kwargs)
if orderby:
self.apply_orderby(orderby)