From 33b011c1589519db8176c9f5a4abb540698902e6 Mon Sep 17 00:00:00 2001 From: Elliot Smith Date: Fri, 15 Jan 2016 13:00:54 +0200 Subject: [PATCH] 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 Signed-off-by: Ed Bartosh Signed-off-by: Richard Purdie --- .../lib/toaster/toastergui/querysetfilter.py | 4 - .../lib/toaster/toastergui/static/js/table.js | 56 +++---- bitbake/lib/toaster/toastergui/tablefilter.py | 140 +++++++++++++++--- bitbake/lib/toaster/toastergui/tables.py | 91 ++++++++---- .../templates/builds-toastertable.html | 2 +- .../toastergui/templates/toastertable.html | 7 +- 6 files changed, 214 insertions(+), 86 deletions(-) diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py b/bitbake/lib/toaster/toastergui/querysetfilter.py index efa8507050..10cc988bce 100644 --- a/bitbake/lib/toaster/toastergui/querysetfilter.py +++ b/bitbake/lib/toaster/toastergui/querysetfilter.py @@ -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() diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js index b0a8ffb8f9..afe16b5e1b 100644 --- a/bitbake/lib/toaster/toastergui/static/js/table.js +++ b/bitbake/lib/toaster/toastergui/static/js/table.js @@ -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 = '
' + '', name: '', - count: + count: , + ... 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); } diff --git a/bitbake/lib/toaster/toastergui/tablefilter.py b/bitbake/lib/toaster/toastergui/tablefilter.py index 1ea30da304..bd8decd0e3 100644 --- a/bitbake/lib/toaster/toastergui/tablefilter.py +++ b/bitbake/lib/toaster/toastergui/tablefilter.py @@ -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 + + {'__gte': , '__lte': } + + 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 diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py index 06ced52eb1..58abe36b05 100644 --- a/bitbake/lib/toaster/toastergui/tables.py +++ b/bitbake/lib/toaster/toastergui/tables.py @@ -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) diff --git a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html index 2e32edb100..bf13a66bd1 100644 --- a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html +++ b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html @@ -18,7 +18,7 @@ {% include 'mrb_section.html' %} {% endwith %} -

+

{% url 'builds' as xhr_table_url %} {% include 'toastertable.html' %} diff --git a/bitbake/lib/toaster/toastergui/templates/toastertable.html b/bitbake/lib/toaster/toastergui/templates/toastertable.html index 98a715f27d..f0a3aedb74 100644 --- a/bitbake/lib/toaster/toastergui/templates/toastertable.html +++ b/bitbake/lib/toaster/toastergui/templates/toastertable.html @@ -32,8 +32,11 @@ - - +