Merge branch 'master' into gh-pages

This commit is contained in:
Rufus Pollock
2012-06-16 17:04:30 +01:00
18 changed files with 1504 additions and 899 deletions

66
dist/recline.js vendored
View File

@@ -2755,12 +2755,19 @@ this.recline = this.recline || {};
this.recline.View = this.recline.View || {}; this.recline.View = this.recline.View || {};
(function($, my) { (function($, my) {
// turn off unnecessary logging from VMM Timeline
VMM.debug = false;
// ## Timeline
//
// Timeline view using http://timeline.verite.co/
my.Timeline = Backbone.View.extend({ my.Timeline = Backbone.View.extend({
tagName: 'div', tagName: 'div',
className: 'recline-timeline',
template: ' \ template: ' \
<div id="vmm-timeline-id"></div> \ <div class="recline-timeline"> \
<div id="vmm-timeline-id"></div> \
</div> \
', ',
// These are the default (case-insensitive) names of field that are used if found. // These are the default (case-insensitive) names of field that are used if found.
@@ -2775,6 +2782,7 @@ my.Timeline = Backbone.View.extend({
this.timeline = new VMM.Timeline(); this.timeline = new VMM.Timeline();
this._timelineIsInitialized = false; this._timelineIsInitialized = false;
this.bind('view:show', function() { this.bind('view:show', function() {
// only call _initTimeline once view in DOM as Timeline uses $ internally to look up element
if (self._timelineIsInitialized === false) { if (self._timelineIsInitialized === false) {
self._initTimeline(); self._initTimeline();
} }
@@ -2794,6 +2802,11 @@ my.Timeline = Backbone.View.extend({
this.state = new recline.Model.ObjectState(stateData); this.state = new recline.Model.ObjectState(stateData);
this._setupTemporalField(); this._setupTemporalField();
this.render(); this.render();
// can only call _initTimeline once view in DOM as Timeline uses $
// internally to look up element
if ($(this.elementId).length > 0) {
this._initTimeline();
}
}, },
render: function() { render: function() {
@@ -2803,9 +2816,12 @@ my.Timeline = Backbone.View.extend({
}, },
_initTimeline: function() { _initTimeline: function() {
var $timeline = this.el.find(this.elementId);
// set width explicitly o/w timeline goes wider that screen for some reason // set width explicitly o/w timeline goes wider that screen for some reason
this.el.find(this.elementId).width(this.el.parent().width()); var width = Math.max(this.el.width(), this.el.find('.recline-timeline').width());
// only call _initTimeline once view in DOM as Timeline uses $ internally to look up element if (width) {
$timeline.width(width);
}
var config = {}; var config = {};
var data = this._timelineJSON(); var data = this._timelineJSON();
this.timeline.init(data, this.elementId, config); this.timeline.init(data, this.elementId, config);
@@ -2819,6 +2835,30 @@ my.Timeline = Backbone.View.extend({
} }
}, },
// Convert record to JSON for timeline
//
// Designed to be overridden in client apps
convertRecord: function(record, fields) {
return this._convertRecord(record, fields);
},
// Internal method to generate a Timeline formatted entry
_convertRecord: function(record, fields) {
var start = this._parseDate(record.get(this.state.get('startField')));
var end = this._parseDate(record.get(this.state.get('endField')));
if (start) {
var tlEntry = {
"startDate": start,
"endDate": end,
"headline": String(record.get('title') || ''),
"text": record.get('description') || record.summary()
};
return tlEntry;
} else {
return null;
}
},
_timelineJSON: function() { _timelineJSON: function() {
var self = this; var self = this;
var out = { var out = {
@@ -2829,17 +2869,10 @@ my.Timeline = Backbone.View.extend({
] ]
} }
}; };
this.model.currentRecords.each(function(doc) { this.model.currentRecords.each(function(record) {
var start = self._parseDate(doc.get(self.state.get('startField'))); var newEntry = self.convertRecord(record, self.fields);
var end = self._parseDate(doc.get(self.state.get('endField'))); if (newEntry) {
if (start) { out.timeline.date.push(newEntry);
var tlEntry = {
"startDate": start,
"endDate": end,
"headline": String(doc.get('title') || ''),
"text": doc.summary()
};
out.timeline.date.push(tlEntry);
} }
}); });
// if no entries create a placeholder entry to prevent Timeline crashing with error // if no entries create a placeholder entry to prevent Timeline crashing with error
@@ -2854,6 +2887,9 @@ my.Timeline = Backbone.View.extend({
}, },
_parseDate: function(date) { _parseDate: function(date) {
if (!date) {
return null;
}
var out = date.trim(); var out = date.trim();
out = out.replace(/(\d)th/g, '$1'); out = out.replace(/(\d)th/g, '$1');
out = out.replace(/(\d)st/g, '$1'); out = out.replace(/(\d)st/g, '$1');

View File

@@ -87,44 +87,57 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
}; };
this._normalizeQuery = function(queryObj) { this._normalizeQuery = function(queryObj) {
var out = queryObj && queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj); var self = this;
if (out.q !== undefined && out.q.trim() === '') { var queryInfo = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
delete out.q; var out = {
} constant_score: {
if (!out.q) { query: {}
out.query = { }
};
if (!queryInfo.q) {
out.constant_score.query = {
match_all: {} match_all: {}
}; };
} else { } else {
out.query = { out.constant_score.query = {
query_string: { query_string: {
query: out.q query: queryInfo.q
} }
}; };
delete out.q;
} }
// now do filters (note the *plural*) if (queryInfo.filters && queryInfo.filters.length) {
if (out.filters && out.filters.length) { out.constant_score.filter = {
if (!out.filter) { and: []
out.filter = {}; };
} _.each(queryInfo.filters, function(filter) {
if (!out.filter.and) { out.constant_score.filter.and.push(self._convertFilter(filter));
out.filter.and = []; });
}
out.filter.and = out.filter.and.concat(out.filters);
}
if (out.filters !== undefined) {
delete out.filters;
} }
return out; return out;
}; },
this._convertFilter = function(filter) {
var out = {};
out[filter.type] = {}
if (filter.type === 'term') {
out.term[filter.field] = filter.term.toLowerCase();
} else if (filter.type === 'geo_distance') {
out.geo_distance[filter.field] = filter.point;
out.geo_distance.distance = filter.distance;
}
return out;
},
// ### query // ### query
// //
// @return deferred supporting promise API // @return deferred supporting promise API
this.query = function(queryObj) { this.query = function(queryObj) {
var esQuery = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
var queryNormalized = this._normalizeQuery(queryObj); var queryNormalized = this._normalizeQuery(queryObj);
var data = {source: JSON.stringify(queryNormalized)}; delete esQuery.q;
delete esQuery.filters;
esQuery.query = queryNormalized;
var data = {source: JSON.stringify(esQuery)};
var url = this.endpoint + '/_search'; var url = this.endpoint + '/_search';
var jqxhr = recline.Backend.makeRequest({ var jqxhr = recline.Backend.makeRequest({
url: url, url: url,
@@ -213,6 +226,12 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
results.hits.facets = results.facets; results.hits.facets = results.facets;
} }
dfd.resolve(results.hits); dfd.resolve(results.hits);
}).fail(function(errorObj) {
var out = {
title: 'Failed: ' + errorObj.status + ' code',
message: errorObj.responseText
};
dfd.reject(out);
}); });
return dfd.promise(); return dfd.promise();
}; };

View File

@@ -74,8 +74,11 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
var fieldName = _.keys(sortObj)[0]; var fieldName = _.keys(sortObj)[0];
results = _.sortBy(results, function(doc) { results = _.sortBy(results, function(doc) {
var _out = doc[fieldName]; var _out = doc[fieldName];
return (sortObj[fieldName].order == 'asc') ? _out : -1*_out; return _out;
}); });
if (sortObj[fieldName].order == 'desc') {
results.reverse();
}
}); });
var total = results.length; var total = results.length;
var facets = this.computeFacets(results, queryObj); var facets = this.computeFacets(results, queryObj);
@@ -90,10 +93,12 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
// in place filtering // in place filtering
this._applyFilters = function(results, queryObj) { this._applyFilters = function(results, queryObj) {
_.each(queryObj.filters, function(filter) { _.each(queryObj.filters, function(filter) {
results = _.filter(results, function(doc) { // if a term filter ...
var fieldId = _.keys(filter.term)[0]; if (filter.type === 'term') {
return (doc[fieldId] == filter.term[fieldId]); results = _.filter(results, function(doc) {
}); return (doc[filter.field] == filter.term);
});
}
}); });
return results; return results;
}; };

View File

@@ -393,7 +393,7 @@ my.FieldList = Backbone.Collection.extend({
// may just pass this straight through e.g. for an SQL backend this could be // may just pass this straight through e.g. for an SQL backend this could be
// the full SQL query // the full SQL query
// //
// * filters: dict of ElasticSearch filters. These will be and-ed together for // * filters: array of ElasticSearch filters. These will be and-ed together for
// execution. // execution.
// //
// **Examples** // **Examples**
@@ -411,12 +411,44 @@ my.Query = Backbone.Model.extend({
return { return {
size: 100, size: 100,
from: 0, from: 0,
q: '',
facets: {}, facets: {},
// <http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html>
// , filter: {}
filters: [] filters: []
}; };
}, },
_filterTemplates: {
term: {
type: 'term',
field: '',
term: ''
},
geo_distance: {
distance: 10,
distance_unit: 'km',
point: {
lon: 0,
lat: 0
}
}
},
// ### addFilter
//
// Add a new filter (appended to the list of filters)
//
// @param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates
addFilter: function(filter) {
// crude deep copy
var ourfilter = JSON.parse(JSON.stringify(filter));
// not full specified so use template and over-write
if (_.keys(filter).length <= 2) {
ourfilter = _.extend(this._filterTemplates[filter.type], ourfilter);
}
var filters = this.get('filters');
filters.push(ourfilter);
this.trigger('change:filters:new-blank');
},
updateFilter: function(index, value) {
},
// #### addTermFilter // #### addTermFilter
// //
// Set (update or add) a terms filter to filters // Set (update or add) a terms filter to filters
@@ -436,6 +468,22 @@ my.Query = Backbone.Model.extend({
this.trigger('change:filters:new-blank'); this.trigger('change:filters:new-blank');
} }
}, },
addGeoDistanceFilter: function(field) {
var filters = this.get('filters');
var filter = {
geo_distance: {
distance: '10km',
}
};
filter.geo_distance[field] = {
'lon': 0,
'lat': 0
};
filters.push(filter);
this.set({filters: filters});
// adding a new blank filter and do not want to trigger a new query
this.trigger('change:filters:new-blank');
},
// ### removeFilter // ### removeFilter
// //
// Remove a filter from filters at index filterIndex // Remove a filter from filters at index filterIndex

View File

@@ -227,38 +227,55 @@ my.Map = Backbone.View.extend({
// Private: Return a GeoJSON geomtry extracted from the record fields // Private: Return a GeoJSON geomtry extracted from the record fields
// //
_getGeometryFromRecord: function(doc){ _getGeometryFromRecord: function(doc){
if (this._geomReady()){ if (this.state.get('geomField')){
if (this.state.get('geomField')){ var value = doc.get(this.state.get('geomField'));
var value = doc.get(this.state.get('geomField')); if (typeof(value) === 'string'){
if (typeof(value) === 'string'){ // We *may* have a GeoJSON string representation
// We *may* have a GeoJSON string representation try {
try { value = $.parseJSON(value);
value = $.parseJSON(value); } catch(e) {}
} catch(e) { }
}
} if (typeof(value) === 'string') {
if (value && value.lat) { value = value.replace('(', '').replace(')', '');
// not yet geojson so convert var parts = value.split(',');
value = { var lat = parseFloat(parts[0]);
"type": "Point", var lon = parseFloat(parts[1]);
"coordinates": [value.lon || value.lng, value.lat] if (!isNaN(lon) && !isNaN(parseFloat(lat))) {
}; return {
} "type": "Point",
// We now assume that contents of the field are a valid GeoJSON object "coordinates": [lon, lat]
return value; };
} else if (this.state.get('lonField') && this.state.get('latField')){ } else {
// We'll create a GeoJSON like point object from the two lat/lon fields return null;
var lon = doc.get(this.state.get('lonField')); }
var lat = doc.get(this.state.get('latField')); } else if (value && value.slice) {
if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) { // [ lon, lat ]
return { return {
type: 'Point', "type": "Point",
coordinates: [lon,lat] "coordinates": [value[0], value[1]]
}; };
} } else if (value && value.lat) {
// of form { lat: ..., lon: ...}
return {
"type": "Point",
"coordinates": [value.lon || value.lng, value.lat]
};
}
// We o/w assume that contents of the field are a valid GeoJSON object
return value;
} else if (this.state.get('lonField') && this.state.get('latField')){
// We'll create a GeoJSON like point object from the two lat/lon fields
var lon = doc.get(this.state.get('lonField'));
var lat = doc.get(this.state.get('latField'));
if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
return {
type: 'Point',
coordinates: [lon,lat]
};
} }
return null;
} }
return null;
}, },
// Private: Check if there is a field with GeoJSON geometries or alternatively, // Private: Check if there is a field with GeoJSON geometries or alternatively,

View File

@@ -135,35 +135,18 @@ my.SlickGrid = Backbone.View.extend({
this.grid = new Slick.Grid(this.el, data, visibleColumns, options); this.grid = new Slick.Grid(this.el, data, visibleColumns, options);
// Column sorting // Column sorting
var gridSorter = function(field, ascending, grid, data){ var sortInfo = this.model.queryState.get('sort');
data.sort(function(a, b){
var result =
a[field] > b[field] ? 1 :
a[field] < b[field] ? -1 :
0
;
return ascending ? result : -result;
});
grid.setData(data);
grid.updateRowCount();
grid.render();
}
var sortInfo = this.state.get('columnsSort');
if (sortInfo){ if (sortInfo){
var sortAsc = !(sortInfo['direction'] == 'desc'); var column = _.keys(sortInfo[0])[0];
gridSorter(sortInfo.column, sortAsc, self.grid, data); var sortAsc = !(sortInfo[0][column].order == 'desc');
this.grid.setSortColumn(sortInfo.column, sortAsc); this.grid.setSortColumn(column, sortAsc);
} }
this.grid.onSort.subscribe(function(e, args){ this.grid.onSort.subscribe(function(e, args){
gridSorter(args.sortCol.field,args.sortAsc,self.grid,data); var order = (args.sortAsc) ? 'asc':'desc';
self.state.set({columnsSort:{ var sort = [{}];
column:args.sortCol, sort[0][args.sortCol.field] = {order: order};
direction: (args.sortAsc) ? 'asc':'desc' self.model.query({sort: sort});
}});
}); });
this.grid.onColumnsReordered.subscribe(function(e, args){ this.grid.onColumnsReordered.subscribe(function(e, args){

View File

@@ -4,12 +4,19 @@ this.recline = this.recline || {};
this.recline.View = this.recline.View || {}; this.recline.View = this.recline.View || {};
(function($, my) { (function($, my) {
// turn off unnecessary logging from VMM Timeline
VMM.debug = false;
// ## Timeline
//
// Timeline view using http://timeline.verite.co/
my.Timeline = Backbone.View.extend({ my.Timeline = Backbone.View.extend({
tagName: 'div', tagName: 'div',
className: 'recline-timeline',
template: ' \ template: ' \
<div id="vmm-timeline-id"></div> \ <div class="recline-timeline"> \
<div id="vmm-timeline-id"></div> \
</div> \
', ',
// These are the default (case-insensitive) names of field that are used if found. // These are the default (case-insensitive) names of field that are used if found.
@@ -24,6 +31,7 @@ my.Timeline = Backbone.View.extend({
this.timeline = new VMM.Timeline(); this.timeline = new VMM.Timeline();
this._timelineIsInitialized = false; this._timelineIsInitialized = false;
this.bind('view:show', function() { this.bind('view:show', function() {
// only call _initTimeline once view in DOM as Timeline uses $ internally to look up element
if (self._timelineIsInitialized === false) { if (self._timelineIsInitialized === false) {
self._initTimeline(); self._initTimeline();
} }
@@ -43,6 +51,11 @@ my.Timeline = Backbone.View.extend({
this.state = new recline.Model.ObjectState(stateData); this.state = new recline.Model.ObjectState(stateData);
this._setupTemporalField(); this._setupTemporalField();
this.render(); this.render();
// can only call _initTimeline once view in DOM as Timeline uses $
// internally to look up element
if ($(this.elementId).length > 0) {
this._initTimeline();
}
}, },
render: function() { render: function() {
@@ -52,9 +65,12 @@ my.Timeline = Backbone.View.extend({
}, },
_initTimeline: function() { _initTimeline: function() {
var $timeline = this.el.find(this.elementId);
// set width explicitly o/w timeline goes wider that screen for some reason // set width explicitly o/w timeline goes wider that screen for some reason
this.el.find(this.elementId).width(this.el.parent().width()); var width = Math.max(this.el.width(), this.el.find('.recline-timeline').width());
// only call _initTimeline once view in DOM as Timeline uses $ internally to look up element if (width) {
$timeline.width(width);
}
var config = {}; var config = {};
var data = this._timelineJSON(); var data = this._timelineJSON();
this.timeline.init(data, this.elementId, config); this.timeline.init(data, this.elementId, config);
@@ -68,6 +84,30 @@ my.Timeline = Backbone.View.extend({
} }
}, },
// Convert record to JSON for timeline
//
// Designed to be overridden in client apps
convertRecord: function(record, fields) {
return this._convertRecord(record, fields);
},
// Internal method to generate a Timeline formatted entry
_convertRecord: function(record, fields) {
var start = this._parseDate(record.get(this.state.get('startField')));
var end = this._parseDate(record.get(this.state.get('endField')));
if (start) {
var tlEntry = {
"startDate": start,
"endDate": end,
"headline": String(record.get('title') || ''),
"text": record.get('description') || record.summary()
};
return tlEntry;
} else {
return null;
}
},
_timelineJSON: function() { _timelineJSON: function() {
var self = this; var self = this;
var out = { var out = {
@@ -78,17 +118,10 @@ my.Timeline = Backbone.View.extend({
] ]
} }
}; };
this.model.currentRecords.each(function(doc) { this.model.currentRecords.each(function(record) {
var start = self._parseDate(doc.get(self.state.get('startField'))); var newEntry = self.convertRecord(record, self.fields);
var end = self._parseDate(doc.get(self.state.get('endField'))); if (newEntry) {
if (start) { out.timeline.date.push(newEntry);
var tlEntry = {
"startDate": start,
"endDate": end,
"headline": String(doc.get('title') || ''),
"text": doc.summary()
};
out.timeline.date.push(tlEntry);
} }
}); });
// if no entries create a placeholder entry to prevent Timeline crashing with error // if no entries create a placeholder entry to prevent Timeline crashing with error
@@ -103,6 +136,9 @@ my.Timeline = Backbone.View.extend({
}, },
_parseDate: function(date) { _parseDate: function(date) {
if (!date) {
return null;
}
var out = date.trim(); var out = date.trim();
out = out.replace(/(\d)th/g, '$1'); out = out.replace(/(\d)th/g, '$1');
out = out.replace(/(\d)st/g, '$1'); out = out.replace(/(\d)st/g, '$1');

View File

@@ -15,7 +15,8 @@ my.FilterEditor = Backbone.View.extend({
<fieldset> \ <fieldset> \
<label>Filter type</label> \ <label>Filter type</label> \
<select class="filterType"> \ <select class="filterType"> \
<option value="term">Term (text) filter</option> \ <option value="term">Term (text)</option> \
<option value="geo_distance">Geo distance</option> \
</select> \ </select> \
<label>Field</label> \ <label>Field</label> \
<select class="fields"> \ <select class="fields"> \
@@ -27,21 +28,44 @@ my.FilterEditor = Backbone.View.extend({
</fieldset> \ </fieldset> \
</form> \ </form> \
<form class="form-stacked js-edit"> \ <form class="form-stacked js-edit"> \
{{#termFilters}} \ {{#filters}} \
<div class="control-group filter-term filter" data-filter-id={{id}}> \ {{{filterRender}}} \
<label class="control-label" for="">{{label}}</label> \ {{/filters}} \
<div class="controls"> \ {{#filters.length}} \
<input type="text" value="{{value}}" name="{{fieldId}}" data-filter-field="{{fieldId}}" data-filter-id="{{id}}" data-filter-type="term" /> \
<a class="js-remove-filter" href="#">&times;</a> \
</div> \
</div> \
{{/termFilters}} \
{{#termFilters.length}} \
<button type="submit" class="btn">Update</button> \ <button type="submit" class="btn">Update</button> \
{{/termFilters.length}} \ {{/filters.length}} \
</form> \ </form> \
</div> \ </div> \
', ',
filterTemplates: {
term: ' \
<div class="filter-{{type}} filter"> \
<fieldset> \
<legend> \
{{field}} <small>{{type}}</small> \
<a class="js-remove-filter" href="#" title="Remove this filter">&times;</a> \
</legend> \
<input type="text" value="{{term}}" name="term" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
</fieldset> \
</div> \
',
geo_distance: ' \
<div class="filter-{{type}} filter"> \
<fieldset> \
<legend> \
{{field}} <small>{{type}}</small> \
<a class="js-remove-filter" href="#" title="Remove this filter">&times;</a> \
</legend> \
<label class="control-label" for="">Longitude</label> \
<input type="text" value="{{point.lon}}" name="lon" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
<label class="control-label" for="">Latitude</label> \
<input type="text" value="{{point.lat}}" name="lat" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
<label class="control-label" for="">Distance (km)</label> \
<input type="text" value="{{distance}}" name="distance" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
</fieldset> \
</div> \
'
},
events: { events: {
'click .js-remove-filter': 'onRemoveFilter', 'click .js-remove-filter': 'onRemoveFilter',
'click .js-add-filter': 'onAddFilterShow', 'click .js-add-filter': 'onAddFilterShow',
@@ -51,30 +75,23 @@ my.FilterEditor = Backbone.View.extend({
initialize: function() { initialize: function() {
this.el = $(this.el); this.el = $(this.el);
_.bindAll(this, 'render'); _.bindAll(this, 'render');
this.model.fields.bind('all', this.render);
this.model.queryState.bind('change', this.render); this.model.queryState.bind('change', this.render);
this.model.queryState.bind('change:filters:new-blank', this.render); this.model.queryState.bind('change:filters:new-blank', this.render);
this.render(); this.render();
}, },
render: function() { render: function() {
var self = this;
var tmplData = $.extend(true, {}, this.model.queryState.toJSON()); var tmplData = $.extend(true, {}, this.model.queryState.toJSON());
// we will use idx in list as there id ... // we will use idx in list as there id ...
tmplData.filters = _.map(tmplData.filters, function(filter, idx) { tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
filter.id = idx; filter.id = idx;
return filter; return filter;
}); });
tmplData.termFilters = _.filter(tmplData.filters, function(filter) {
return filter.term !== undefined;
});
tmplData.termFilters = _.map(tmplData.termFilters, function(filter) {
var fieldId = _.keys(filter.term)[0];
return {
id: filter.id,
fieldId: fieldId,
label: fieldId,
value: filter.term[fieldId]
};
});
tmplData.fields = this.model.fields.toJSON(); tmplData.fields = this.model.fields.toJSON();
tmplData.filterRender = function() {
return Mustache.render(self.filterTemplates[this.type], this);
};
var out = Mustache.render(this.template, tmplData); var out = Mustache.render(this.template, tmplData);
this.el.html(out); this.el.html(out);
}, },
@@ -90,9 +107,7 @@ my.FilterEditor = Backbone.View.extend({
$target.hide(); $target.hide();
var filterType = $target.find('select.filterType').val(); var filterType = $target.find('select.filterType').val();
var field = $target.find('select.fields').val(); var field = $target.find('select.fields').val();
if (filterType === 'term') { this.model.queryState.addFilter({type: filterType, field: field});
this.model.queryState.addTermFilter(field);
}
// trigger render explicitly as queryState change will not be triggered (as blank value for filter) // trigger render explicitly as queryState change will not be triggered (as blank value for filter)
this.render(); this.render();
}, },
@@ -109,10 +124,20 @@ my.FilterEditor = Backbone.View.extend({
var $form = $(e.target); var $form = $(e.target);
_.each($form.find('input'), function(input) { _.each($form.find('input'), function(input) {
var $input = $(input); var $input = $(input);
var filterIndex = parseInt($input.attr('data-filter-id')); var filterType = $input.attr('data-filter-type');
var value = $input.val();
var fieldId = $input.attr('data-filter-field'); var fieldId = $input.attr('data-filter-field');
filters[filterIndex].term[fieldId] = value; var filterIndex = parseInt($input.attr('data-filter-id'));
var name = $input.attr('name');
var value = $input.val();
if (filterType === 'term') {
filters[filterIndex].term = value;
} else if (filterType === 'geo_distance') {
if (name === 'distance') {
filters[filterIndex].distance = parseFloat(value);
} else {
filters[filterIndex].point[name] = parseFloat(value);
}
}
}); });
self.model.queryState.set({filters: filters}); self.model.queryState.set({filters: filters});
self.model.queryState.trigger('change'); self.model.queryState.trigger('change');

View File

@@ -3,32 +3,77 @@ module("Backend ElasticSearch - Wrapper");
test("queryNormalize", function() { test("queryNormalize", function() {
var backend = new recline.Backend.ElasticSearch.Wrapper(); var backend = new recline.Backend.ElasticSearch.Wrapper();
var in_ = new recline.Model.Query(); var in_ = new recline.Model.Query();
var out = backend._normalizeQuery(in_); var out = backend._normalizeQuery(in_);
equal(out.size, 100); var exp = {
constant_score: {
query: {
match_all: {}
}
}
};
deepEqual(out, exp);
var in_ = new recline.Model.Query(); var in_ = new recline.Model.Query();
in_.set({q: ''}); in_.set({q: ''});
var out = backend._normalizeQuery(in_); var out = backend._normalizeQuery(in_);
equal(out.q, undefined); deepEqual(out, exp);
deepEqual(out.query.match_all, {});
var in_ = new recline.Model.Query().toJSON();
in_.q = '';
var out = backend._normalizeQuery(in_);
equal(out.q, undefined);
deepEqual(out.query.match_all, {});
var in_ = new recline.Model.Query().toJSON();
in_.q = 'abc';
var out = backend._normalizeQuery(in_);
equal(out.query.query_string.query, 'abc');
var in_ = new recline.Model.Query(); var in_ = new recline.Model.Query();
in_.addTermFilter('xyz', 'XXX'); in_.attributes.q = 'abc';
in_ = in_.toJSON();
var out = backend._normalizeQuery(in_); var out = backend._normalizeQuery(in_);
deepEqual(out.filter.and[0], {term: { xyz: 'XXX'}}); equal(out.constant_score.query.query_string.query, 'abc');
var in_ = new recline.Model.Query();
in_.addFilter({
type: 'term',
field: 'xyz',
term: 'XXX'
});
var out = backend._normalizeQuery(in_);
var exp = {
constant_score: {
query: {
match_all: {}
},
filter: {
and: [
{
term: {
xyz: 'XXX'
}
}
]
}
}
};
deepEqual(out, exp);
var in_ = new recline.Model.Query();
in_.addFilter({
type: 'geo_distance',
field: 'xyz'
});
var out = backend._normalizeQuery(in_);
var exp = {
constant_score: {
query: {
match_all: {}
},
filter: {
and: [
{
geo_distance: {
distance: '10km',
'xyz': { lon: 0, lat: 0 }
}
}
]
}
}
};
deepEqual(out, exp);
}); });
var mapping_data = { var mapping_data = {
@@ -128,6 +173,7 @@ test("query", function() {
return { return {
done: function(callback) { done: function(callback) {
callback(sample_data); callback(sample_data);
return this;
}, },
fail: function() { fail: function() {
} }
@@ -224,10 +270,11 @@ test("query", function() {
return { return {
done: function(callback) { done: function(callback) {
callback(sample_data); callback(sample_data);
return this;
}, },
fail: function() { fail: function() {
} }
} };
} }
}); });

View File

@@ -44,6 +44,22 @@ test('query sort', function () {
}; };
var out = data.query(queryObj); var out = data.query(queryObj);
equal(out.records[0].x, 6); equal(out.records[0].x, 6);
var queryObj = {
sort: [
{'country': {order: 'desc'}}
]
};
var out = data.query(queryObj);
equal(out.records[0].country, 'US');
var queryObj = {
sort: [
{'country': {order: 'asc'}}
]
};
var out = data.query(queryObj);
equal(out.records[0].country, 'DE');
}); });
test('query string', function () { test('query string', function () {
@@ -60,7 +76,7 @@ test('query string', function () {
test('filters', function () { test('filters', function () {
var data = _wrapData(); var data = _wrapData();
var query = new recline.Model.Query(); var query = new recline.Model.Query();
query.addTermFilter('country', 'UK'); query.addFilter({type: 'term', field: 'country', term: 'UK'});
var out = data.query(query.toJSON()); var out = data.query(query.toJSON());
equal(out.total, 3); equal(out.total, 3);
deepEqual(_.pluck(out.records, 'country'), ['UK', 'UK', 'UK']); deepEqual(_.pluck(out.records, 'country'), ['UK', 'UK', 'UK']);
@@ -198,7 +214,7 @@ test('query string', function () {
test('filters', function () { test('filters', function () {
var dataset = makeBackendDataset(); var dataset = makeBackendDataset();
dataset.queryState.addTermFilter('country', 'UK'); dataset.queryState.addFilter({type: 'term', field: 'country', term: 'UK'});
dataset.query().then(function() { dataset.query().then(function() {
equal(dataset.currentRecords.length, 3); equal(dataset.currentRecords.length, 3);
deepEqual(dataset.currentRecords.pluck('country'), ['UK', 'UK', 'UK']); deepEqual(dataset.currentRecords.pluck('country'), ['UK', 'UK', 'UK']);

View File

@@ -150,6 +150,29 @@ test('Query', function () {
}); });
test('Query.addFilter', function () { test('Query.addFilter', function () {
var query = new recline.Model.Query();
query.addFilter({type: 'term', field: 'xyz'});
var exp = {
field: 'xyz',
type: 'term',
term: ''
};
deepEqual(query.get('filters')[0], exp);
query.addFilter({type: 'geo_distance', field: 'xyz'});
var exp = {
distance: '10km',
point: {
lon: 0,
lat: 0
},
field: 'xyz',
type: 'geo_distance'
};
deepEqual(exp, query.get('filters')[1]);
});
test('Query.addTermFilter', function () {
var query = new recline.Model.Query(); var query = new recline.Model.Query();
query.addTermFilter('xyz', 'this-value'); query.addTermFilter('xyz', 'this-value');
deepEqual({term: {xyz: 'this-value'}}, query.get('filters')[0]); deepEqual({term: {xyz: 'this-value'}}, query.get('filters')[0]);

View File

@@ -1,9 +1,9 @@
/** /**
* QUnit - A JavaScript Unit Testing Framework * QUnit v1.6.0 - A JavaScript Unit Testing Framework
* *
* http://docs.jquery.com/QUnit * http://docs.jquery.com/QUnit
* *
* Copyright (c) 2011 John Resig, Jörn Zaefferer * Copyright (c) 2012 John Resig, Jörn Zaefferer
* Dual licensed under the MIT (MIT-LICENSE.txt) * Dual licensed under the MIT (MIT-LICENSE.txt)
* or GPL (GPL-LICENSE.txt) licenses. * or GPL (GPL-LICENSE.txt) licenses.
*/ */
@@ -54,6 +54,11 @@
color: #fff; color: #fff;
} }
#qunit-header label {
display: inline-block;
padding-left: 0.5em;
}
#qunit-banner { #qunit-banner {
height: 5px; height: 5px;
} }
@@ -186,6 +191,7 @@
color: #710909; color: #710909;
background-color: #fff; background-color: #fff;
border-left: 26px solid #EE5757; border-left: 26px solid #EE5757;
white-space: pre;
} }
#qunit-tests > li:last-child { #qunit-tests > li:last-child {
@@ -215,6 +221,9 @@
border-bottom: 1px solid white; border-bottom: 1px solid white;
} }
#qunit-testresult .module-name {
font-weight: bold;
}
/** Fixture */ /** Fixture */
@@ -222,4 +231,6 @@
position: absolute; position: absolute;
top: -10000px; top: -10000px;
left: -10000px; left: -10000px;
width: 1000px;
height: 1000px;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,7 @@ test('initialize', function () {
}); });
test('dates in graph view', function () { test('dates in graph view', function () {
expect(0);
var dataset = Fixture.getDataset(); var dataset = Fixture.getDataset();
var view = new recline.View.Graph({ var view = new recline.View.Graph({
model: dataset, model: dataset,

View File

@@ -105,21 +105,25 @@ test('GeoJSON geom field', function () {
view.remove(); view.remove();
}); });
test('geom field non-GeoJSON', function () { test('_getGeometryFromRecord non-GeoJSON', function () {
var data = [{ var test = [
location: { lon: 47, lat: 53}, [{ lon: 47, lat: 53}, [47,53]],
title: 'abc' ["53.3,47.32", [47.32, 53.3]],
}]; ["53.3, 47.32", [47.32, 53.3]],
var dataset = recline.Backend.Memory.createDataset(data); ["(53.3,47.32)", [47.32, 53.3]],
[[53.3,47.32], [53.3, 47.32]]
];
var view = new recline.View.Map({ var view = new recline.View.Map({
model: dataset model: recline.Backend.Memory.createDataset([{a: 1}]),
state: {
geomField: 'location'
}
});
_.each(test, function(item) {
var record = new recline.Model.Record({location: item[0]});
var out = view._getGeometryFromRecord(record);
deepEqual(out.coordinates, item[1]);
}); });
//Fire query, otherwise the map won't be initialized
dataset.query();
// Check that all features were created
equal(_getFeaturesCount(view.features), 1);
}); });
test('Popup', function () { test('Popup', function () {

View File

@@ -29,7 +29,6 @@ test('state', function () {
state: { state: {
hiddenColumns:['x','lat','title'], hiddenColumns:['x','lat','title'],
columnsOrder:['lon','id','z','date', 'y', 'country'], columnsOrder:['lon','id','z','date', 'y', 'country'],
columnsSort:{column:'country',direction:'desc'},
columnsWidth:[ columnsWidth:[
{column:'id',width: 250} {column:'id',width: 250}
] ]
@@ -52,9 +51,6 @@ test('state', function () {
// Column order // Column order
deepEqual(_.pluck(headers,'title'),view.state.get('columnsOrder')); deepEqual(_.pluck(headers,'title'),view.state.get('columnsOrder'));
// Column sorting
equal($(view.grid.getCellNode(0,view.grid.getColumnIndex('country'))).text(),'US');
// Column width // Column width
equal($('.slick-header-column[title="id"]').width(),250); equal($('.slick-header-column[title="id"]').width(),250);

View File

@@ -59,7 +59,8 @@ test('_parseDate', function () {
[ 'August 1st 1914', '1914-08-01T00:00:00.000Z' ], [ 'August 1st 1914', '1914-08-01T00:00:00.000Z' ],
[ '1914-08-01', '1914-08-01T00:00:00.000Z' ], [ '1914-08-01', '1914-08-01T00:00:00.000Z' ],
[ '1914-08-01T08:00', '1914-08-01T08:00:00.000Z' ], [ '1914-08-01T08:00', '1914-08-01T08:00:00.000Z' ],
[ 'afdaf afdaf', null ] [ 'afdaf afdaf', null ],
[ null, null ]
]; ];
_.each(testData, function(item) { _.each(testData, function(item) {
var out = view._parseDate(item[0]); var out = view._parseDate(item[0]);

View File

@@ -21,12 +21,12 @@ test('basics', function () {
ok(!$addForm.is(":visible")); ok(!$addForm.is(":visible"));
$editForm = view.el.find('form.js-edit'); $editForm = view.el.find('form.js-edit');
equal($editForm.find('.filter-term').length, 1) equal($editForm.find('.filter-term').length, 1)
equal(_.keys(dataset.queryState.attributes.filters[0].term)[0], 'country'); equal(dataset.queryState.attributes.filters[0].field, 'country');
// now set filter value and apply // now set filter value and apply
$editForm.find('input').val('UK'); $editForm.find('input').val('UK');
$editForm.submit(); $editForm.submit();
equal(dataset.queryState.attributes.filters[0].term.country, 'UK'); equal(dataset.queryState.attributes.filters[0].term, 'UK');
equal(dataset.currentRecords.length, 3); equal(dataset.currentRecords.length, 3);
// now remove filter // now remove filter
@@ -39,3 +39,29 @@ test('basics', function () {
view.remove(); view.remove();
}); });
test('geo_distance', function () {
var dataset = Fixture.getDataset();
var view = new recline.View.FilterEditor({
model: dataset
});
$('.fixtures').append(view.el);
var $addForm = view.el.find('form.js-add');
// submit the form
$addForm.find('select.filterType').val('geo_distance');
$addForm.find('select.fields').val('lon');
$addForm.submit();
// now check we have new filter
$editForm = view.el.find('form.js-edit');
equal($editForm.find('.filter-geo_distance').length, 1)
deepEqual(_.keys(dataset.queryState.attributes.filters[0]), ['distance',
'point', 'type', 'field']);
// now set filter value and apply
$editForm.find('input[name="lat"]').val(10);
$editForm.submit();
equal(dataset.queryState.attributes.filters[0].point.lat, 10);
view.remove();
});