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:
parent
b929889cdd
commit
f8d383d87f
|
@ -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
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue