bitbake: toastergui: implement "today" and "yesterday" filters

Add the "today" and "yesterday" filters to the started_on
and completed_on columns in the builds table.

During this work, some minor adjustments were made to the
behaviour of the builds table:

* Amend filter action variable names so they're more succinct.
* Retain order in which actions are added to a filter, as this
ordering is used in the UI when displaying the filter actions.
* Always show the table chrome, otherwise it's not possible
to edit the columns shown until there are 10 or more results.
* Because date range searches may return no results, make sure
that the search bar and "show all results" link are visible
when the query returns no results.

[YOCTO #8738]

(Bitbake rev: f17cfa009e58833e0e55884fa04de8abd522b6bc)

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:54 +02:00 committed by Richard Purdie
parent f8d383d87f
commit 33b011c158
6 changed files with 214 additions and 86 deletions

View File

@ -22,7 +22,3 @@ class QuerysetFilter(object):
return queryset.filter(self.criteria)
else:
return queryset
def count(self, queryset):
""" Returns a count of the elements in the filtered queryset """
return self.filter(queryset).count()

View File

@ -71,22 +71,11 @@ function tableInit(ctx){
if (tableData.total === 0){
tableContainer.hide();
/* If we were searching show the new search bar and return */
if (tableParams.search){
$("#new-search-input-"+ctx.tableName).val(tableParams.search);
$("#no-results-"+ctx.tableName).show();
}
$("#new-search-input-"+ctx.tableName).val(tableParams.search);
$("#no-results-"+ctx.tableName).show();
table.trigger("table-done", [tableData.total, tableParams]);
return;
/* We don't want to clutter the place with the table chrome if there
* are only a few results */
} else if (tableData.total <= 10 &&
!tableParams.filter &&
!tableParams.search){
$("#table-chrome-"+ctx.tableName).hide();
pagination.hide();
} else {
tableContainer.show();
$("#no-results-"+ctx.tableName).hide();
@ -399,13 +388,14 @@ function tableInit(ctx){
/**
* Create the DOM/JS for the client side of a TableFilterActionToggle
* or TableFilterActionDay
*
* 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) {
function createActionRadio(filterName, filterActionData) {
var actionStr = '<div class="radio">' +
'<input type="radio" name="filter"' +
' value="' + filterName + '"';
@ -471,8 +461,7 @@ function tableInit(ctx){
minDate: new Date(filterActionData.min)
};
// create date pickers, setting currently-selected from and to
// dates
// create date pickers, setting currently-selected from and to dates
var selectedFrom = null;
var selectedTo = null;
@ -496,6 +485,20 @@ function tableInit(ctx){
action.find('[data-date-to-for]').datepicker(options);
inputTo.val(selectedTo);
// if the radio button is checked and one or both of the datepickers are
// empty, populate them with today's date
radio.change(function () {
var now = new Date();
if (inputFrom.val() === '') {
inputFrom.datepicker('setDate', now);
}
if (inputTo.val() === '') {
inputTo.datepicker('setDate', now);
}
});
// set filter_value based on date pickers when
// one of their values changes
var changeHandler = function () {
@ -553,7 +556,8 @@ function tableInit(ctx){
{
title: '<label for radio button inside the popup>',
name: '<name of the filter action>',
count: <number of items this filter will show>
count: <number of items this filter will show>,
... additional data for the action ...
}
]
}
@ -567,11 +571,12 @@ function tableInit(ctx){
filter
the filterName is set on the column filter icon, and corresponds
to a value in the table's filters property
to a value in the table's filter map
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
querystring, along with a filter_value, and applied to the
queryset on the table
*/
var filterActionRadios = $('#filter-actions-' + ctx.tableName);
@ -587,10 +592,12 @@ function tableInit(ctx){
var filterName = filterData.name + ':' +
filterActionData.action_name;
if (filterActionData.type === 'toggle') {
action = createActionToggle(filterName, filterActionData);
if (filterActionData.type === 'toggle' ||
filterActionData.type === 'day') {
action = createActionRadio(filterName, filterActionData);
}
else if (filterActionData.type === 'daterange') {
// current values for the from/to dates
var filterValue = tableParams.filter_value;
action = createActionDateRange(
@ -601,7 +608,7 @@ function tableInit(ctx){
}
if (action) {
// Setup the current selected filter, default to 'all' if
// Setup the current selected filter; default to 'all' if
// no current filter selected
var radioInput = action.children('input[name="filter"]');
if ((tableParams.filter &&
@ -707,13 +714,12 @@ function tableInit(ctx){
tableParams.filter + "']");
tableParams.filter_value = checkedFilterValue.val();
var filterBtn = $("#" + tableParams.filter.split(":")[0]);
/* All === remove filter */
if (tableParams.filter.match(":all$")) {
tableParams.filter = null;
filterBtnActive(filterBtn, false);
tableParams.filter_value = null;
} else {
var filterBtn = $("#" + tableParams.filter.split(":")[0]);
filterBtnActive(filterBtn, true);
}

View File

@ -18,13 +18,18 @@
# 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
from datetime import timedelta
from querysetfilter import QuerysetFilter
class TableFilter(object):
"""
Stores a filter for a named field, and can retrieve the action
requested from the set of actions for that filter
requested from the set of actions for that filter;
the order in which actions are added governs the order in which they
are returned in the JSON for the filter
"""
def __init__(self, name, title):
@ -32,7 +37,11 @@ class TableFilter(object):
self.title = title
self.__filter_action_map = {}
# retains the ordering of actions
self.__filter_action_keys = []
def add_action(self, action):
self.__filter_action_keys.append(action.name)
self.__filter_action_map[action.name] = action
def get_action(self, action_name):
@ -56,7 +65,8 @@ class TableFilter(object):
})
# add other filter actions
for action_name, filter_action in self.__filter_action_map.iteritems():
for action_name in self.__filter_action_keys:
filter_action = self.__filter_action_map[action_name]
obj = filter_action.to_json(queryset)
obj['action_name'] = action_name
filter_actions.append(obj)
@ -67,6 +77,40 @@ class TableFilter(object):
'filter_actions': filter_actions
}
class TableFilterQueryHelper(object):
def dateStringsToQ(self, field_name, date_from_str, date_to_str):
"""
Convert the date strings from_date_str and to_date_str into a
set of args in the form
{'<field_name>__gte': <date from>, '<field_name>__lte': <date to>}
where date_from and date_to are Django-timezone-aware dates; then
convert that into a Django Q object
Returns the Q object based on those criteria
"""
# one of the values required for the filter is missing, so set
# it to the one which was supplied
if date_from_str == '':
date_from_str = date_to_str
elif date_to_str == '':
date_to_str = date_from_str
date_from_naive = dateparse.parse_datetime(date_from_str + ' 00:00:00')
date_to_naive = dateparse.parse_datetime(date_to_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[field_name + '__gte'] = date_from
args[field_name + '__lte'] = date_to
return Q(**args)
class TableFilterAction(object):
"""
A filter action which displays in the filter popup for a ToasterTable
@ -99,7 +143,7 @@ class TableFilterAction(object):
return {
'title': self.title,
'type': self.type,
'count': self.queryset_filter.count(queryset)
'count': self.filter(queryset).count()
}
class TableFilterActionToggle(TableFilterAction):
@ -113,15 +157,70 @@ class TableFilterActionToggle(TableFilterAction):
super(TableFilterActionToggle, self).__init__(*args)
self.type = 'toggle'
class TableFilterActionDay(TableFilterAction):
"""
A filter action which filters according to the named datetime field and a
string representing a day ("today" or "yesterday")
"""
TODAY = 'today'
YESTERDAY = 'yesterday'
def __init__(self, name, title, field, day,
queryset_filter = QuerysetFilter(), query_helper = TableFilterQueryHelper()):
"""
field: (string) the datetime field to filter by
day: (string) "today" or "yesterday"
"""
super(TableFilterActionDay, self).__init__(
name,
title,
queryset_filter
)
self.type = 'day'
self.field = field
self.day = day
self.query_helper = query_helper
def filter(self, queryset):
"""
Apply the day filtering before returning the queryset;
this is done here as the value of the filter criteria changes
depending on when the filtering is applied
"""
criteria = None
date_str = None
now = timezone.now()
if self.day == self.YESTERDAY:
increment = timedelta(days=1)
wanted_date = now - increment
else:
wanted_date = now
wanted_date_str = wanted_date.strftime('%Y-%m-%d')
criteria = self.query_helper.dateStringsToQ(
self.field,
wanted_date_str,
wanted_date_str
)
self.queryset_filter.set_criteria(criteria)
return self.queryset_filter.filter(queryset)
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):
def __init__(self, name, title, field,
queryset_filter = QuerysetFilter(), query_helper = TableFilterQueryHelper()):
"""
field: the field to find the max/min range from in the queryset
field: (string) the field to find the max/min range from in the queryset
"""
super(TableFilterActionDateRange, self).__init__(
name,
@ -131,9 +230,13 @@ class TableFilterActionDateRange(TableFilterAction):
self.type = 'daterange'
self.field = field
self.query_helper = query_helper
def set_filter_params(self, params):
"""
This filter depends on the user selecting some input, so it needs
to have its parameters set before its queryset is filtered
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
@ -143,30 +246,18 @@ class TableFilterActionDateRange(TableFilterAction):
# if params are invalid, return immediately, resetting criteria
# on the QuerysetFilter
try:
from_date_str, to_date_str = params.split(',')
date_from_str, date_to_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)
criteria = self.query_helper.dateStringsToQ(
self.field,
date_from_str,
date_to_str
)
self.queryset_filter.set_criteria(criteria)
def to_json(self, queryset):
@ -179,7 +270,8 @@ class TableFilterActionDateRange(TableFilterAction):
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
# will select depends on the date range entered and we don't know
# that ahead of time
data['count'] = None
return data

View File

@ -32,6 +32,7 @@ import itertools
from toastergui.tablefilter import TableFilter
from toastergui.tablefilter import TableFilterActionToggle
from toastergui.tablefilter import TableFilterActionDateRange
from toastergui.tablefilter import TableFilterActionDay
class ProjectFilters(object):
def __init__(self, project_layers):
@ -65,20 +66,20 @@ class LayersTable(ToasterTable):
criteria = Q(projectlayer__in=self.project_layers)
in_project_filter_action = TableFilterActionToggle(
in_project_action = TableFilterActionToggle(
"in_project",
"Layers added to this project",
QuerysetFilter(criteria)
)
not_in_project_filter_action = TableFilterActionToggle(
not_in_project_action = TableFilterActionToggle(
"not_in_project",
"Layers not added to this project",
QuerysetFilter(~criteria)
)
in_current_project_filter.add_action(in_project_filter_action)
in_current_project_filter.add_action(not_in_project_filter_action)
in_current_project_filter.add_action(in_project_action)
in_current_project_filter.add_action(not_in_project_action)
self.add_filter(in_current_project_filter)
def setup_queryset(self, *args, **kwargs):
@ -221,20 +222,20 @@ class MachinesTable(ToasterTable):
"Filter by project machines"
)
in_project_filter_action = TableFilterActionToggle(
in_project_action = TableFilterActionToggle(
"in_project",
"Machines provided by layers added to this project",
project_filters.in_project
)
not_in_project_filter_action = TableFilterActionToggle(
not_in_project_action = TableFilterActionToggle(
"not_in_project",
"Machines provided by layers not added to this project",
project_filters.not_in_project
)
in_current_project_filter.add_action(in_project_filter_action)
in_current_project_filter.add_action(not_in_project_filter_action)
in_current_project_filter.add_action(in_project_action)
in_current_project_filter.add_action(not_in_project_action)
self.add_filter(in_current_project_filter)
def setup_queryset(self, *args, **kwargs):
@ -354,20 +355,20 @@ class RecipesTable(ToasterTable):
'Filter by project recipes'
)
in_project_filter_action = TableFilterActionToggle(
in_project_action = TableFilterActionToggle(
'in_project',
'Recipes provided by layers added to this project',
project_filters.in_project
)
not_in_project_filter_action = TableFilterActionToggle(
not_in_project_action = TableFilterActionToggle(
'not_in_project',
'Recipes provided by layers not added to this project',
project_filters.not_in_project
)
table_filter.add_action(in_project_filter_action)
table_filter.add_action(not_in_project_filter_action)
table_filter.add_action(in_project_action)
table_filter.add_action(not_in_project_action)
self.add_filter(table_filter)
def setup_queryset(self, *args, **kwargs):
@ -1137,20 +1138,20 @@ class BuildsTable(ToasterTable):
'Filter builds by outcome'
)
successful_builds_filter_action = TableFilterActionToggle(
successful_builds_action = TableFilterActionToggle(
'successful_builds',
'Successful builds',
QuerysetFilter(Q(outcome=Build.SUCCEEDED))
)
failed_builds_filter_action = TableFilterActionToggle(
failed_builds_action = TableFilterActionToggle(
'failed_builds',
'Failed builds',
QuerysetFilter(Q(outcome=Build.FAILED))
)
outcome_filter.add_action(successful_builds_filter_action)
outcome_filter.add_action(failed_builds_filter_action)
outcome_filter.add_action(successful_builds_action)
outcome_filter.add_action(failed_builds_action)
self.add_filter(outcome_filter)
# started on
@ -1159,14 +1160,29 @@ class BuildsTable(ToasterTable):
'Filter by date when build was started'
)
by_started_date_range_filter_action = TableFilterActionDateRange(
'date_range',
'Build date range',
started_today_action = TableFilterActionDay(
'today',
'Today\'s builds',
'started_on',
QuerysetFilter()
'today'
)
started_on_filter.add_action(by_started_date_range_filter_action)
started_yesterday_action = TableFilterActionDay(
'yesterday',
'Yesterday\'s builds',
'started_on',
'yesterday'
)
by_started_date_range_action = TableFilterActionDateRange(
'date_range',
'Build date range',
'started_on'
)
started_on_filter.add_action(started_today_action)
started_on_filter.add_action(started_yesterday_action)
started_on_filter.add_action(by_started_date_range_action)
self.add_filter(started_on_filter)
# completed on
@ -1175,14 +1191,29 @@ class BuildsTable(ToasterTable):
'Filter by date when build was completed'
)
by_completed_date_range_filter_action = TableFilterActionDateRange(
'date_range',
'Build date range',
completed_today_action = TableFilterActionDay(
'today',
'Today\'s builds',
'completed_on',
QuerysetFilter()
'today'
)
completed_on_filter.add_action(by_completed_date_range_filter_action)
completed_yesterday_action = TableFilterActionDay(
'yesterday',
'Yesterday\'s builds',
'completed_on',
'yesterday'
)
by_completed_date_range_action = TableFilterActionDateRange(
'date_range',
'Build date range',
'completed_on'
)
completed_on_filter.add_action(completed_today_action)
completed_on_filter.add_action(completed_yesterday_action)
completed_on_filter.add_action(by_completed_date_range_action)
self.add_filter(completed_on_filter)
# failed tasks
@ -1193,18 +1224,18 @@ class BuildsTable(ToasterTable):
criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
with_failed_tasks_filter_action = TableFilterActionToggle(
with_failed_tasks_action = TableFilterActionToggle(
'with_failed_tasks',
'Builds with failed tasks',
QuerysetFilter(criteria)
)
without_failed_tasks_filter_action = TableFilterActionToggle(
without_failed_tasks_action = TableFilterActionToggle(
'without_failed_tasks',
'Builds without failed tasks',
QuerysetFilter(~criteria)
)
failed_tasks_filter.add_action(with_failed_tasks_filter_action)
failed_tasks_filter.add_action(without_failed_tasks_filter_action)
failed_tasks_filter.add_action(with_failed_tasks_action)
failed_tasks_filter.add_action(without_failed_tasks_action)
self.add_filter(failed_tasks_filter)

View File

@ -18,7 +18,7 @@
{% include 'mrb_section.html' %}
{% endwith %}
<h1 class="page-header top-air" data-role="page-title"></h1>
<h1 class="page-header top-air" data-role="page-title"></h1>
{% url 'builds' as xhr_table_url %}
{% include 'toastertable.html' %}

View File

@ -32,8 +32,11 @@
<a href="#" class="add-on btn remove-search-btn-{{table_name}}" tabindex="-1">
<i class="icon-remove"></i>
</a>
<button class="btn search-submit-{{table_name}}" >Search</button>
<button class="btn btn-link remove-search-btn-{{table_name}}">Show {{title|lower}}
<button class="btn search-submit-{{table_name}}">
Search
</button>
<button class="btn btn-link show-all-{{table_name}}">
Show {{title|lower}}
</button>
</form>
</div>