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] Signed-off-by: Elliot Smith <[email protected]> --- bitbake/lib/toaster/toastergui/querysetfilter.py | 4 - bitbake/lib/toaster/toastergui/static/js/table.js | 56 +++++---- bitbake/lib/toaster/toastergui/tablefilter.py | 139 +++++++++++++++++---- bitbake/lib/toaster/toastergui/tables.py | 87 ++++++++----- .../toastergui/templates/builds-toastertable.html | 2 +- .../toaster/toastergui/templates/toastertable.html | 7 +- 6 files changed, 211 insertions(+), 84 deletions(-) diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py b/bitbake/lib/toaster/toastergui/querysetfilter.py index efa8507..10cc988 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 c619956..d77ebaf 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 = '<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); } diff --git a/bitbake/lib/toaster/toastergui/tablefilter.py b/bitbake/lib/toaster/toastergui/tablefilter.py index eb053ac..6cbf975 100644 --- a/bitbake/lib/toaster/toastergui/tablefilter.py +++ b/bitbake/lib/toaster/toastergui/tablefilter.py @@ -1,10 +1,14 @@ +from datetime import timedelta from django.db.models import Q, Max, Min from django.utils import dateparse, timezone +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): @@ -12,7 +16,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): @@ -36,7 +44,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) @@ -47,6 +56,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 @@ -79,7 +122,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): @@ -93,15 +136,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, @@ -111,9 +209,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 @@ -123,30 +225,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): @@ -159,7 +249,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 a9efc0c..b76d1aa 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): @@ -1139,20 +1140,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 @@ -1161,14 +1162,29 @@ class BuildsTable(ToasterTable): 'Filter by date when build was started' ) - by_started_date_range_filter_action = TableFilterActionDateRange( + started_today_action = TableFilterActionDay( + 'today', + 'Today\'s builds', + 'started_on', + 'today' + ) + + started_yesterday_action = TableFilterActionDay( + 'yesterday', + 'Yesterday\'s builds', + 'started_on', + 'yesterday' + ) + + by_started_date_range_action = TableFilterActionDateRange( 'date_range', 'Build date range', - 'started_on', - QuerysetFilter() + 'started_on' ) - started_on_filter.add_action(by_started_date_range_filter_action) + 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 @@ -1177,14 +1193,29 @@ class BuildsTable(ToasterTable): 'Filter by date when build was completed' ) - by_completed_date_range_filter_action = TableFilterActionDateRange( + completed_today_action = TableFilterActionDay( + 'today', + 'Today\'s builds', + 'completed_on', + 'today' + ) + + completed_yesterday_action = TableFilterActionDay( + 'yesterday', + 'Yesterday\'s builds', + 'completed_on', + 'yesterday' + ) + + by_completed_date_range_action = TableFilterActionDateRange( 'date_range', 'Build date range', - 'completed_on', - QuerysetFilter() + 'completed_on' ) - completed_on_filter.add_action(by_completed_date_range_filter_action) + 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 @@ -1195,18 +1226,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 2e32edb..bf13a66 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 %} - <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' %} diff --git a/bitbake/lib/toaster/toastergui/templates/toastertable.html b/bitbake/lib/toaster/toastergui/templates/toastertable.html index 98a715f..f0a3aed 100644 --- a/bitbake/lib/toaster/toastergui/templates/toastertable.html +++ b/bitbake/lib/toaster/toastergui/templates/toastertable.html @@ -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> -- Elliot Smith Software Engineer Intel OTC --------------------------------------------------------------------- Intel Corporation (UK) Limited Registered No. 1134945 (England) Registered Office: Pipers Way, Swindon SN3 1RJ VAT No: 860 2173 47 This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). Any review or distribution by others is strictly prohibited. If you are not the intended recipient, please contact the sender and delete all copies. -- _______________________________________________ toaster mailing list [email protected] https://lists.yoctoproject.org/listinfo/toaster
