summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorElliot Smith <elliot.smith@intel.com>2016-01-15 13:00:53 +0200
committerRichard Purdie <richard.purdie@linuxfoundation.org>2016-01-15 16:30:00 +0000
commitf8d383d87f0b9d4a4c9ae7b1a6c8ceebf90ef9b0 (patch)
treecda7dc3eb23a5b6a97241c965bbd9b0dcddc02eb
parentb929889cdd4a36846f9569d89fabd9987e94b39e (diff)
downloadpoky-f8d383d87f0b9d4a4c9ae7b1a6c8ceebf90ef9b0.tar.gz
poky-f8d383d87f0b9d4a4c9ae7b1a6c8ceebf90ef9b0.tar.bz2
poky-f8d383d87f0b9d4a4c9ae7b1a6c8ceebf90ef9b0.zip
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>
-rw-r--r--bitbake/lib/toaster/toastergui/querysetfilter.py3
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/table.js196
-rw-r--r--bitbake/lib/toaster/toastergui/tablefilter.py113
-rw-r--r--bitbake/lib/toaster/toastergui/tables.py38
-rw-r--r--bitbake/lib/toaster/toastergui/templates/builds-toastertable.html32
-rw-r--r--bitbake/lib/toaster/toastergui/widgets.py32
6 files changed, 330 insertions, 84 deletions
diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py b/bitbake/lib/toaster/toastergui/querysetfilter.py
index dbae239370..efa8507050 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 63f8a1fed7..b0a8ffb8f9 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 b42fd52865..1ea30da304 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 0941637704..06ced52eb1 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 f7604fd7a4..2e32edb100 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 8790340db9..47de30d631 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)