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]
Signed-off-by: Elliot Smith <[email protected]>
---
bitbake/lib/toaster/toastergui/querysetfilter.py | 3 +-
bitbake/lib/toaster/toastergui/static/js/table.js | 196 +++++++++++++++++----
bitbake/lib/toaster/toastergui/tablefilter.py | 113 ++++++++++--
bitbake/lib/toaster/toastergui/tables.py | 38 +++-
.../toastergui/templates/builds-toastertable.html | 32 +---
bitbake/lib/toaster/toastergui/widgets.py | 32 ++--
6 files changed, 330 insertions(+), 84 deletions(-)
diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py
b/bitbake/lib/toaster/toastergui/querysetfilter.py
index dbae239..efa8507 100644
--- a/bitbake/lib/toaster/toastergui/querysetfilter.py
+++ b/bitbake/lib/toaster/toastergui/querysetfilter.py
@@ -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
diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js
b/bitbake/lib/toaster/toastergui/static/js/table.js
index 63f8a1f..b0a8ffb 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -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
+ ')';
-
- 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);
+ if (filterActionData.type === 'toggle') {
+ action = createActionToggle(filterName, filterActionData);
+ }
+ else if (filterActionData.type === 'daterange') {
+ var filterValue = tableParams.filter_value;
+
+ action = createActionDateRange(
+ filterName,
+ filterValue,
+ filterActionData
+ );
+ }
- /* Setup the current selected filter, default to 'all' if
- * no current filter selected.
- */
+ 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()) ||
- filterAction.action_name == 'all') {
+ filterActionData.action_name == 'all') {
radioInput.attr("checked", "checked");
}
- }
- if (action) {
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]);
diff --git a/bitbake/lib/toaster/toastergui/tablefilter.py
b/bitbake/lib/toaster/toastergui/tablefilter.py
index b42fd52..1ea30da 100644
--- a/bitbake/lib/toaster/toastergui/tablefilter.py
+++ b/bitbake/lib/toaster/toastergui/tablefilter.py
@@ -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
+
+ # set in subclasses
+ self.type = None
- def set_params(self, params):
+ 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 = {}
diff --git a/bitbake/lib/toaster/toastergui/tables.py
b/bitbake/lib/toaster/toastergui/tables.py
index 0941637..06ced52 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -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',
diff --git a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
index f7604fd..2e32edb 100644
--- a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
+++ b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
@@ -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 %}
diff --git a/bitbake/lib/toaster/toastergui/widgets.py
b/bitbake/lib/toaster/toastergui/widgets.py
index 8790340..47de30d 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -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)
--
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