Merge branch 'master' into gh-pages

This commit is contained in:
Rufus Pollock 2012-06-02 19:03:38 +01:00
commit e78ddb7bb5
36 changed files with 1090 additions and 978 deletions

View File

@ -35,6 +35,8 @@ Possible breaking changes:
* State only stores backend (name) and dataset url (in url field) rather than entire dataset object
* Backends heavily reorganized
* Rename Document -> Record
* Rename DataExplorer view to MultiView
### v0.4 - April 26th 2012

View File

@ -5,10 +5,10 @@
<link rel="stylesheet" href="vendor/slickgrid/2.0.1/slick.grid.css">
<!-- Recline CSS components -->
<link rel="stylesheet" href="css/data-explorer.css">
<link rel="stylesheet" href="css/grid.css">
<link rel="stylesheet" href="css/graph.css">
<link rel="stylesheet" href="css/map.css">
<link rel="stylesheet" href="css/multiview.css">
<!-- /Recline CSS components -->
<!-- 3rd party JS libraries -->

View File

@ -19,11 +19,11 @@
<link rel="stylesheet" href="../vendor/timeline/20120520/css/timeline.css">
<!-- Recline CSS components -->
<link rel="stylesheet" href="../css/data-explorer.css">
<link rel="stylesheet" href="../css/grid.css">
<link rel="stylesheet" href="../css/slickgrid.css">
<link rel="stylesheet" href="../css/graph.css">
<link rel="stylesheet" href="../css/map.css">
<link rel="stylesheet" href="../css/multiview.css">
<!-- /Recline CSS components -->
<!-- Custom CSS for the Data Explorer Online App -->
@ -49,7 +49,6 @@
<!-- recline library -->
<!-- in normal use would just the single recline.js library file. However, for testing it
is easier to reference individual files. See built.html for example using just recline.js -->
<script type="text/javascript" src="../src/util.js"></script>
<script type="text/javascript" src="../src/costco.js"></script>
<script type="text/javascript" src="../src/model.js"></script>
<script type="text/javascript" src="../src/backend/base.js"></script>
@ -58,13 +57,18 @@
<script type="text/javascript" src="../src/backend/elasticsearch.js"></script>
<script type="text/javascript" src="../src/backend/gdocs.js"></script>
<script type="text/javascript" src="../src/backend/csv.js"></script>
<script type="text/javascript" src="../src/view.js"></script>
<script type="text/javascript" src="../src/view-grid.js"></script>
<script type="text/javascript" src="../src/view-slickgrid.js"></script>
<script type="text/javascript" src="../src/view-transform-dialog.js"></script>
<script type="text/javascript" src="../src/view-graph.js"></script>
<script type="text/javascript" src="../src/view-map.js"></script>
<script type="text/javascript" src="../src/view-transform-dialog.js"></script>
<script type="text/javascript" src="../src/view-timeline.js"></script>
<script type="text/javascript" src="../src/widget.pager.js"></script>
<script type="text/javascript" src="../src/widget.queryeditor.js"></script>
<script type="text/javascript" src="../src/widget.filtereditor.js"></script>
<script type="text/javascript" src="../src/widget.facetviewer.js"></script>
<script type="text/javascript" src="../src/view.multiview.js"></script>
<!-- non-library javascript specific to this demo -->
<script type="text/javascript" src="js/app.js"></script>

View File

@ -21,7 +21,7 @@ var ExplorerApp = Backbone.View.extend({
this.router.route(/explorer/, 'explorer', this.viewExplorer);
Backbone.history.start();
var state = recline.Util.parseQueryString(decodeURIComponent(window.location.search));
var state = recline.View.parseQueryString(decodeURIComponent(window.location.search));
if (state) {
_.each(state, function(value, key) {
try {
@ -108,7 +108,7 @@ var ExplorerApp = Backbone.View.extend({
}
];
this.dataExplorer = new recline.View.DataExplorer({
this.dataExplorer = new recline.View.MultiView({
model: dataset,
el: $el,
state: state,
@ -145,7 +145,7 @@ var ExplorerApp = Backbone.View.extend({
},
makePermaLink: function(state) {
var qs = recline.Util.composeQueryString(state.toJSON());
var qs = recline.View.composeQueryString(state.toJSON());
return window.location.origin + window.location.pathname + qs;
},

View File

@ -27,7 +27,7 @@
.header .recline-results-info {
line-height: 28px;
margin-left: 20px;
display: inline;
float: left;
}
/**********************************************************
@ -39,42 +39,54 @@
height: 30px;
}
.header .recline-query-editor .input-prepend {
.header .input-prepend {
margin-bottom: auto;
}
.recline-query-editor .add-on {
.header .add-on {
float: left;
}
/* needed for Chrome but not FF */
.header .recline-query-editor .add-on {
.header .add-on {
margin-left: -27px;
}
/* needed for FF but not chrome */
.header .recline-query-editor .input-prepend {
.header .input-prepend {
vertical-align: top;
}
.header .recline-query-editor .pagination input {
width: 30px;
height: 18px;
padding: 2px 4px;
margin-top: -4px;
}
.header .recline-query-editor .pagination a {
line-height: 26px;
padding: 0 6px;
}
.header .recline-query-editor form button {
vertical-align: top;
}
/**********************************************************
* Query Editor
* Pager
*********************************************************/
.header .recline-pager {
float: left;
margin: auto;
display: block;
margin-left: 20px;
}
.header .recline-pager .pagination input {
width: 30px;
height: 18px;
padding: 2px 4px;
margin: 0;
margin-top: -4px;
}
.header .recline-pager .pagination a {
line-height: 26px;
padding: 0 6px;
}
/**********************************************************
* Filter Editor
*********************************************************/
.recline-filter-editor .filter-term .input-append a {

100
library-view.markdown Normal file
View File

@ -0,0 +1,100 @@
---
layout: container
title: Library - Views
---
<div class="page-header">
<h1>
Recline Views
</h1>
</div>
Recline Views are instances of Backbone Views and they act as 'WUI' (web user
interface) component displaying some model object in the DOM. Like all Backbone
views they have a pointer to a model (or a collection) and have an associated
DOM-style element (usually this element will be bound into the page at some
point).
Views provided by core Recline are crudely divided into two types:
* Dataset Views: a View intended for displaying a recline.Model.Dataset in some
fashion. Examples are the Grid, Graph and Map views.
* Widget Views: a widget used for displaying some specific (and smaller) aspect
of a dataset or the application. Examples are QueryEditor and FilterEditor
which both provide a way for editing (a part of) a `recline.Model.Query`
associated to a Dataset.
## Dataset View
These views are just Backbone views with a few additional conventions:
1. The model passed to the View should always be a recline.Model.Dataset
instance
2. Views should generate their own root element rather than having it passed
in.
3. Views should apply a css class named 'recline-{view-name-lower-cased} to the
root element (and for all CSS for this view to be qualified using this CSS
class)
4. Read-only mode: CSS for this view should respect/utilize a parent
recline-read-only class in order to trigger read-only behaviour (this class
will usually be set on some parent element of the view's root element).
5. State: state (configuration) information for the view should be stored on an
attribute named state that is an instance of a Backbone Model (or, more
speficially, be an instance of `recline.Model.ObjectState`). In addition, a
state attribute may be specified in the Hash passed to a View on
iniitialization and this information should be used to set the initial state
of the view.
Example of state would be the set of fields being plotted in a graph view.
More information about State can be found below.
To summarize some of this, the initialize function for a Dataset View should
look like:
<pre>
initialize: {
model: {a recline.Model.Dataset instance}
// el: {do not specify - instead view should create}
state: {(optional) Object / Hash specifying initial state}
...
}
</pre>
Note: Dataset Views in core Recline have a common layout on disk as follows,
where ViewName is the named of View class:
<pre>
src/view-{lower-case-ViewName}.js
css/{lower-case-ViewName}.css
test/view-{lower-case-ViewName}.js
</pre>
### State
State information exists in order to support state serialization into the url
or elsewhere and reloading of application from a stored state.
State is available not only for individual views (as described above) but for
the dataset (e.g. the current query). For an example of pulling together state
from across multiple components see `recline.View.DataExplorer`.
### Flash Messages / Notifications
To send 'flash messages' or notifications the convention is that views should
fire an event named `recline:flash` with a payload that is a flash object with
the following attributes (all optional):
* message: message to show.
* category: warning (default), success, error
* persist: if true alert is persistent, o/w hidden after 3s (default=false)
* loader: if true show a loading message
Objects or views wishing to bind to flash messages may then subscribe to these
events and take some action such as displaying them to the user. For an example
of such behaviour see the DataExplorer view.
### Writing your own Views
See the existing Views.

View File

@ -13,7 +13,7 @@ title: Library - Home
<p>Building on <a href="http://backbonejs.com/">Backbone</a>, Recline
supplies components and structure to data-heavy applications by providing a
set of models (Dataset, Document/Row, Field) and views (Grid, Map, Graph
set of models (Dataset, Record/Row, Field) and views (Grid, Map, Graph
etc).</p>
<h2 id="examples">Examples</h2>
@ -52,7 +52,7 @@ title: Library - Home
<h4>Create a new Backend</h4>
</div>
<div class="span3 well">
<h4>Create a Custom Document Object</h4>
<h4>Create a Custom Record Object</h4>
</div>
</div>
@ -70,10 +70,10 @@ title: Library - Home
<p>There are two main model objects:</p>
<ul>
<li><a href="docs/model.html#dataset">Dataset</a>: represents the dataset.
Holds dataset info and a pointer to list of data items (Documents in our
Holds dataset info and a pointer to list of data items (Records in our
terminology) which it can load from the relevant Backend.</li>
<li><a href="docs/model.html#document">Document</a>: an individual data item
(e.g. a row from a relational database or a spreadsheet, a document from from
<li><a href="docs/model.html#record">Record</a>: an individual data item
(e.g. a row from a relational database or a spreadsheet, a record from from
a document DB like CouchDB or MongoDB).</li>
</ul>
@ -84,7 +84,7 @@ title: Library - Home
<li><a href="docs/model.html#query">Query</a>: an object to encapsulate a
query to the backend (useful both for creating queries and for storing and
manipulating query state - e.g. from a query editor).</li>
<li><a href="docs/model.html#facte">Facet</a>: Object to store Facet
<li><a href="docs/model.html#facet">Facet</a>: Object to store Facet
information, that is summary information (e.g. values and counts) about a
field obtained by some faceting method on the backend.</li>
</ul>
@ -97,13 +97,13 @@ title: Library - Home
<div class="span6">
<h4>Backends</h4>
<p>Backends connect Dataset and Documents to data from a
<p>Backends connect Dataset and Records to data from a
specific 'Backend' data source. They provide methods for loading and saving
Datasets and individuals Documents as well as for bulk loading via a query API
Datasets and individuals Records as well as for bulk loading via a query API
and doing bulk transforms on the backend.</p>
<p>A template Base class can be found <a href="docs/backend/base.html">in the
Backend base module of the source docs</a>. It documents both the relevant
Backend base module of the source docs</a>. It records both the relevant
methods a Backend must have and (optionally) provides a base 'class' for
inheritance. You can also find detailed examples of backend implementations in
the source documentation below.</p>
@ -112,7 +112,7 @@ title: Library - Home
<p>Complementing the model are various Views (you can
also easily write your own). Each view holds a pointer to a Dataset:</p>
<ul>
<li>DataExplorer: the parent view which manages the overall app and sets up
<li>MultiView: the parent view which manages the overall app and sets up
sub views.</li>
<li>Grid: the data grid view.</li>
<li>Graph: a simple graphing view using <a
@ -138,7 +138,7 @@ title: Library - Home
<h4>Models and Views (Widgets)</h4>
<ul>
<li><a href="docs/model.html">Models</a></li>
<li><a href="docs/view.html">DataExplorer View (plus common view code)</a></li>
<li><a href="docs/view.multiview.html">MultiView View (plus common view code)</a></li>
<li><a href="docs/view-grid.html">(Data) Grid View</a></li>
<li><a href="docs/view-graph.html">Graph View (based on Flot)</a></li>
<li><a href="docs/view-map.html">Map View (based on Leaflet)</a></li>

2
make
View File

@ -13,7 +13,7 @@ def docs():
print("** Building docs")
docco_executable = os.environ.get('DOCCO_EXECUTABLE','docco')
cmd = '%s src/model.js src/view.js src/view-grid.js src/view-graph.js src/view-map.js' % (docco_executable)
cmd = '%s src/*.js' % (docco_executable)
os.system(cmd)
if os.path.exists('/tmp/recline-docs'):
shutil.rmtree('/tmp/recline-docs')

View File

@ -29,14 +29,14 @@ this.recline.Backend.Base = function() {
// ### sync
//
// An implementation of Backbone.sync that will be used to override
// Backbone.sync on operations for Datasets and Documents which are using this backend.
// Backbone.sync on operations for Datasets and Records which are using this backend.
//
// For read-only implementations you will need only to implement read method
// for Dataset models (and even this can be a null operation). The read method
// should return relevant metadata for the Dataset. We do not require read support
// for Documents because they are loaded in bulk by the query method.
// for Records because they are loaded in bulk by the query method.
//
// For backends supporting write operations you must implement update and delete support for Document objects.
// For backends supporting write operations you must implement update and delete support for Record objects.
//
// All code paths should return an object conforming to the jquery promise API.
this.sync = function(method, model, options) {
@ -44,8 +44,8 @@ this.recline.Backend.Base = function() {
// ### query
//
// Query the backend for documents returning them in bulk. This method will
// be used by the Dataset.query method to search the backend for documents,
// Query the backend for records returning them in bulk. This method will
// be used by the Dataset.query method to search the backend for records,
// retrieving the results in bulk.
//
// @param {recline.model.Dataset} model: Dataset model.
@ -71,11 +71,11 @@ this.recline.Backend.Base = function() {
// <pre>
// {
// total: // (required) total number of results (can be null)
// hits: [ // (required) one entry for each result document
// hits: [ // (required) one entry for each result record
// {
// _score: // (optional) match score for document
// _type: // (optional) document type
// _source: // (required) document/row object
// _score: // (optional) match score for record
// _type: // (optional) record type
// _source: // (required) record/row object
// }
// ],
// facets: { // (optional)

View File

@ -39,7 +39,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
// ### get
//
// Get document corresponding to specified id
// Get record corresponding to specified id
//
// @return promise compatible deferred object.
this.get = function(id) {
@ -52,7 +52,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
// ### upsert
//
// create / update a document to ElasticSearch backend
// create / update a record to ElasticSearch backend
//
// @param {Object} doc an object to insert to the index.
// @return deferred supporting promise API
@ -72,7 +72,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
// ### delete
//
// Delete a document from the ElasticSearch backend.
// Delete a record from the ElasticSearch backend.
//
// @param {Object} id id of object to delete
// @return deferred supporting promise API
@ -154,7 +154,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
// Backbone sync implementation for this backend.
//
// URL of ElasticSearch endpoint to use must be specified on the dataset
// (and on a Document via its dataset attribute) by the dataset having a
// (and on a Record via its dataset attribute) by the dataset having a
// url attribute.
this.sync = function(method, model, options) {
if (model.__type__ == 'Dataset') {
@ -180,15 +180,15 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
dfd.reject(arguments);
});
return dfd.promise();
} else if (model.__type__ == 'Document') {
} else if (model.__type__ == 'Record') {
return es.get(model.dataset.id);
}
} else if (method === 'update') {
if (model.__type__ == 'Document') {
if (model.__type__ == 'Record') {
return es.upsert(model.toJSON());
}
} else if (method === 'delete') {
if (model.__type__ == 'Document') {
if (model.__type__ == 'Record') {
return es.delete(model.id);
}
}

View File

@ -7,7 +7,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
//
// Convenience function to create a simple 'in-memory' dataset in one step.
//
// @param data: list of hashes for each document/row in the data ({key:
// @param data: list of hashes for each record/row in the data ({key:
// value, key: value})
// @param fields: (optional) list of field hashes (each hash defining a hash
// as per recline.Model.Field). If fields not specified they will be taken
@ -76,7 +76,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
results = results.slice(start, start+numRows);
return {
total: total,
documents: results,
records: results,
facets: facets
};
};
@ -118,7 +118,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
return results;
};
this.computeFacets = function(documents, queryObj) {
this.computeFacets = function(records, queryObj) {
var facetResults = {};
if (!queryObj.facets) {
return facetResults;
@ -129,7 +129,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
facetResults[facetId].termsall = {};
});
// faceting
_.each(documents, function(doc) {
_.each(records, function(doc) {
_.each(queryObj.facets, function(query, facetId) {
var fieldId = query.terms.field;
var val = doc[fieldId];
@ -172,13 +172,13 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
}
return dfd.promise();
} else if (method === 'update') {
if (model.__type__ == 'Document') {
if (model.__type__ == 'Record') {
model.dataset._dataCache.update(model.toJSON());
dfd.resolve(model);
}
return dfd.promise();
} else if (method === 'delete') {
if (model.__type__ == 'Document') {
if (model.__type__ == 'Record') {
model.dataset._dataCache.delete(model.toJSON());
dfd.resolve(model);
}
@ -191,7 +191,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
this.query = function(model, queryObj) {
var dfd = $.Deferred();
var results = model._dataCache.query(queryObj);
var hits = _.map(results.documents, function(row) {
var hits = _.map(results.records, function(row) {
return { _source: row };
});
var out = {

View File

@ -12,11 +12,11 @@ this.recline.Model = this.recline.Model || {};
// fields on this Dataset (this can be set explicitly, or, will be set by
// Dataset.fetch() or Dataset.query()
//
// @property {DocumentList} currentDocuments: a `DocumentList` containing the
// Documents we have currently loaded for viewing (updated by calling query
// @property {RecordList} currentRecords: a `RecordList` containing the
// Records we have currently loaded for viewing (updated by calling query
// method)
//
// @property {number} docCount: total number of documents in this dataset
// @property {number} docCount: total number of records in this dataset
//
// @property {Backend} backend: the Backend (instance) for this Dataset.
//
@ -48,7 +48,7 @@ my.Dataset = Backbone.Model.extend({
this.backend = this._backendFromString(backend);
}
this.fields = new my.FieldList();
this.currentDocuments = new my.DocumentList();
this.currentRecords = new my.RecordList();
this.facets = new my.FacetList();
this.docCount = null;
this.queryState = new my.Query();
@ -58,12 +58,12 @@ my.Dataset = Backbone.Model.extend({
// ### query
//
// AJAX method with promise API to get documents from the backend.
// AJAX method with promise API to get records from the backend.
//
// It will query based on current query state (given by this.queryState)
// updated by queryObj (if provided).
//
// Resulting DocumentList are used to reset this.currentDocuments and are
// Resulting RecordList are used to reset this.currentRecords and are
// also returned.
query: function(queryObj) {
var self = this;
@ -73,12 +73,12 @@ my.Dataset = Backbone.Model.extend({
this.backend.query(this, actualQuery).done(function(queryResult) {
self.docCount = queryResult.total;
var docs = _.map(queryResult.hits, function(hit) {
var _doc = new my.Document(hit._source);
var _doc = new my.Record(hit._source);
_doc.backend = self.backend;
_doc.dataset = self;
return _doc;
});
self.currentDocuments.reset(docs);
self.currentRecords.reset(docs);
if (queryResult.facets) {
var facets = _.map(queryResult.facets, function(facetResult, facetId) {
facetResult.id = facetId;
@ -87,7 +87,7 @@ my.Dataset = Backbone.Model.extend({
self.facets.reset(facets);
}
self.trigger('query:done');
dfd.resolve(self.currentDocuments);
dfd.resolve(self.currentRecords);
})
.fail(function(arguments) {
self.trigger('query:fail', arguments);
@ -176,11 +176,11 @@ my.Dataset.restore = function(state) {
return dataset;
};
// ## <a id="document">A Document (aka Row)</a>
// ## <a id="record">A Record (aka Row)</a>
//
// A single entry or row in the dataset
my.Document = Backbone.Model.extend({
__type__: 'Document',
my.Record = Backbone.Model.extend({
__type__: 'Record',
initialize: function() {
_.bindAll(this, 'getFieldValue');
},
@ -188,7 +188,7 @@ my.Document = Backbone.Model.extend({
// ### getFieldValue
//
// For the provided Field get the corresponding rendered computed data value
// for this document.
// for this record.
getFieldValue: function(field) {
var val = this.get(field.id);
if (field.deriver) {
@ -211,17 +211,17 @@ my.Document = Backbone.Model.extend({
}
});
// ## A Backbone collection of Documents
my.DocumentList = Backbone.Collection.extend({
__type__: 'DocumentList',
model: my.Document
// ## A Backbone collection of Records
my.RecordList = Backbone.Collection.extend({
__type__: 'RecordList',
model: my.Record
});
// ## <a id="field">A Field (aka Column) on a Dataset</a>
//
// Following (Backbone) attributes as standard:
//
// * id: a unique identifer for this field- usually this should match the key in the documents hash
// * id: a unique identifer for this field- usually this should match the key in the records hash
// * label: (optional: defaults to id) the visible label used for this field
// * type: (optional: defaults to string) the type of the data in this field. Should be a string as per type names defined by ElasticSearch - see Types list on <http://www.elasticsearch.org/guide/reference/mapping/>
// * format: (optional) used to indicate how the data should be formatted. For example:
@ -234,13 +234,13 @@ my.DocumentList = Backbone.Collection.extend({
//
// @property {Function} renderer: a function to render the data for this field.
// Signature: function(value, field, doc) where value is the value of this
// cell, field is corresponding field object and document is the document
// cell, field is corresponding field object and record is the record
// object. Note that implementing functions can ignore arguments (e.g.
// function(value) would be a valid formatter function).
//
// @property {Function} deriver: a function to derive/compute the value of data
// in this field as a function of this field's value (if any) and the current
// document, its signature and behaviour is the same as for renderer. Use of
// record, its signature and behaviour is the same as for renderer. Use of
// this function allows you to define an entirely new value for data in this
// field. This provides support for a) 'derived/computed' fields: i.e. fields
// whose data are functions of the data in other fields b) transforming the
@ -461,7 +461,7 @@ my.Query = Backbone.Model.extend({
// "_type" : "terms",
// // total number of tokens in the facet
// "total": 5,
// // @property {number} number of documents which have no value for the field
// // @property {number} number of records which have no value for the field
// "missing" : 0,
// // number of facet values not included in the returned facets
// "other": 0,

View File

@ -1,79 +0,0 @@
/*jshint multistr:true */
this.recline = this.recline || {};
this.recline.Util = this.recline.Util || {};
(function(my) {
// ## Miscellaneous Utilities
var urlPathRegex = /^([^?]+)(\?.*)?/;
// Parse the Hash section of a URL into path and query string
my.parseHashUrl = function(hashUrl) {
var parsed = urlPathRegex.exec(hashUrl);
if (parsed === null) {
return {};
} else {
return {
path: parsed[1],
query: parsed[2] || ''
};
}
};
// Parse a URL query string (?xyz=abc...) into a dictionary.
my.parseQueryString = function(q) {
if (!q) {
return {};
}
var urlParams = {},
e, d = function (s) {
return unescape(s.replace(/\+/g, " "));
},
r = /([^&=]+)=?([^&]*)/g;
if (q && q.length && q[0] === '?') {
q = q.slice(1);
}
while (e = r.exec(q)) {
// TODO: have values be array as query string allow repetition of keys
urlParams[d(e[1])] = d(e[2]);
}
return urlParams;
};
// Parse the query string out of the URL hash
my.parseHashQueryString = function() {
q = my.parseHashUrl(window.location.hash).query;
return my.parseQueryString(q);
};
// Compse a Query String
my.composeQueryString = function(queryParams) {
var queryString = '?';
var items = [];
$.each(queryParams, function(key, value) {
if (typeof(value) === 'object') {
value = JSON.stringify(value);
}
items.push(key + '=' + encodeURIComponent(value));
});
queryString += items.join('&');
return queryString;
};
my.getNewHashForQueryString = function(queryParams) {
var queryPart = my.composeQueryString(queryParams);
if (window.location.hash) {
// slice(1) to remove # at start
return window.location.hash.split('?')[0].slice(1) + queryPart;
} else {
return queryPart;
}
};
my.setHashQueryString = function(queryParams) {
window.location.hash = my.getNewHashForQueryString(queryParams);
};
})(this.recline.Util);

View File

@ -97,8 +97,8 @@ my.Graph = Backbone.View.extend({
this.model.bind('change', this.render);
this.model.fields.bind('reset', this.render);
this.model.fields.bind('add', this.render);
this.model.currentDocuments.bind('add', this.redraw);
this.model.currentDocuments.bind('reset', this.redraw);
this.model.currentRecords.bind('add', this.redraw);
this.model.currentRecords.bind('reset', this.redraw);
// because we cannot redraw when hidden we may need when becoming visible
this.bind('view:show', function() {
if (this.needToRedraw) {
@ -181,7 +181,7 @@ my.Graph = Backbone.View.extend({
// Uncaught Invalid dimensions for plot, width = 0, height = 0
// * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
if ((!areWeVisible || this.model.currentDocuments.length === 0)) {
if ((!areWeVisible || this.model.currentRecords.length === 0)) {
this.needToRedraw = true;
return;
}
@ -209,8 +209,8 @@ my.Graph = Backbone.View.extend({
// However, that is non-trivial to work out from a dataset (datasets may
// have no field type info). Thus at present we only do this for bars.
var tickFormatter = function (val) {
if (self.model.currentDocuments.models[val]) {
var out = self.model.currentDocuments.models[val].get(self.state.attributes.group);
if (self.model.currentRecords.models[val]) {
var out = self.model.currentRecords.models[val].get(self.state.attributes.group);
// if the value was in fact a number we want that not the
if (typeof(out) == 'number') {
return val;
@ -266,7 +266,7 @@ my.Graph = Backbone.View.extend({
tickLength: 1,
tickFormatter: tickFormatter,
min: -0.5,
max: self.model.currentDocuments.length - 0.5
max: self.model.currentRecords.length - 0.5
}
}
};
@ -304,8 +304,8 @@ my.Graph = Backbone.View.extend({
y = _tmp;
}
// convert back from 'index' value on x-axis (e.g. in cases where non-number values)
if (self.model.currentDocuments.models[x]) {
x = self.model.currentDocuments.models[x].get(self.state.attributes.group);
if (self.model.currentRecords.models[x]) {
x = self.model.currentRecords.models[x].get(self.state.attributes.group);
} else {
x = x.toFixed(2);
}
@ -339,7 +339,7 @@ my.Graph = Backbone.View.extend({
var series = [];
_.each(this.state.attributes.series, function(field) {
var points = [];
_.each(self.model.currentDocuments.models, function(doc, index) {
_.each(self.model.currentRecords.models, function(doc, index) {
var xfield = self.model.fields.get(self.state.attributes.group);
var x = doc.getFieldValue(xfield);
// time series

View File

@ -17,9 +17,9 @@ my.Grid = Backbone.View.extend({
var self = this;
this.el = $(this.el);
_.bindAll(this, 'render', 'onHorizontalScroll');
this.model.currentDocuments.bind('add', this.render);
this.model.currentDocuments.bind('reset', this.render);
this.model.currentDocuments.bind('remove', this.render);
this.model.currentRecords.bind('add', this.render);
this.model.currentRecords.bind('reset', this.render);
this.model.currentRecords.bind('remove', this.render);
this.tempState = {};
var state = _.extend({
hiddenFields: []
@ -77,13 +77,13 @@ my.Grid = Backbone.View.extend({
showColumn: function() { self.showColumn(e); },
deleteRow: function() {
var self = this;
var doc = _.find(self.model.currentDocuments.models, function(doc) {
var doc = _.find(self.model.currentRecords.models, function(doc) {
// important this is == as the currentRow will be string (as comes
// from DOM) while id may be int
return doc.id == self.tempState.currentRow;
});
doc.destroy().then(function() {
self.model.currentDocuments.remove(doc);
self.model.currentRecords.remove(doc);
self.trigger('recline:flash', {message: "Row deleted successfully"});
}).fail(function(err) {
self.trigger('recline:flash', {message: "Errorz! " + err});
@ -213,7 +213,7 @@ my.Grid = Backbone.View.extend({
});
var htmls = Mustache.render(this.template, this.toTemplateJSON());
this.el.html(htmls);
this.model.currentDocuments.forEach(function(doc) {
this.model.currentRecords.forEach(function(doc) {
var tr = $('<tr />');
self.el.find('tbody').append(tr);
var newView = new my.GridRow({
@ -246,7 +246,7 @@ my.Grid = Backbone.View.extend({
}
});
// ## GridRow View for rendering an individual document.
// ## GridRow View for rendering an individual record.
//
// Since we want this to update in place it is up to creator to provider the element to attach to.
//
@ -256,7 +256,7 @@ my.Grid = Backbone.View.extend({
//
// <pre>
// var row = new GridRow({
// model: dataset-document,
// model: dataset-record,
// el: dom-element,
// fields: mydatasets.fields // a FieldList object
// });

View File

@ -7,7 +7,7 @@ this.recline.View = this.recline.View || {};
// ## Map view for a Dataset using Leaflet mapping library.
//
// This view allows to plot gereferenced documents on a map. The location
// This view allows to plot gereferenced records on a map. The location
// information can be provided either via a field with
// [GeoJSON](http://geojson.org) objects or two fields with latitude and
// longitude coordinates.
@ -115,14 +115,14 @@ my.Map = Backbone.View.extend({
self.render()
});
// Listen to changes in the documents
this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
this.model.currentDocuments.bind('change', function(doc){
// Listen to changes in the records
this.model.currentRecords.bind('add', function(doc){self.redraw('add',doc)});
this.model.currentRecords.bind('change', function(doc){
self.redraw('remove',doc);
self.redraw('add',doc);
});
this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)});
this.model.currentDocuments.bind('reset', function(){self.redraw('reset')});
this.model.currentRecords.bind('remove', function(doc){self.redraw('remove',doc)});
this.model.currentRecords.bind('reset', function(){self.redraw('reset')});
this.bind('view:show',function(){
// If the div was hidden, Leaflet needs to recalculate some sizes
@ -184,9 +184,9 @@ my.Map = Backbone.View.extend({
// Actions can be:
//
// * reset: Clear all features
// * add: Add one or n features (documents)
// * remove: Remove one or n features (documents)
// * refresh: Clear existing features and add all current documents
// * add: Add one or n features (records)
// * remove: Remove one or n features (records)
// * refresh: Clear existing features and add all current records
redraw: function(action, doc){
var self = this;
action = action || 'refresh';
@ -201,7 +201,7 @@ my.Map = Backbone.View.extend({
if (this.geomReady && this.mapReady){
if (action == 'reset' || action == 'refresh'){
this.features.clearLayers();
this._add(this.model.currentDocuments.models);
this._add(this.model.currentRecords.models);
} else if (action == 'add' && doc){
this._add(doc);
} else if (action == 'remove' && doc){
@ -266,11 +266,11 @@ my.Map = Backbone.View.extend({
// Private: Add one or n features to the map
//
// For each document passed, a GeoJSON geometry will be extracted and added
// For each record passed, a GeoJSON geometry will be extracted and added
// to the features layer. If an exception is thrown, the process will be
// stopped and an error notification shown.
//
// Each feature will have a popup associated with all the document fields.
// Each feature will have a popup associated with all the record fields.
//
_add: function(docs){
var self = this;
@ -281,7 +281,7 @@ my.Map = Backbone.View.extend({
var wrongSoFar = 0;
_.every(docs,function(doc){
count += 1;
var feature = self._getGeometryFromDocument(doc);
var feature = self._getGeometryFromRecord(doc);
if (typeof feature === 'undefined' || feature === null){
// Empty field
return true;
@ -338,22 +338,28 @@ my.Map = Backbone.View.extend({
},
// Private: Return a GeoJSON geomtry extracted from the document fields
// Private: Return a GeoJSON geomtry extracted from the record fields
//
_getGeometryFromDocument: function(doc){
_getGeometryFromRecord: function(doc){
if (this.geomReady){
if (this.state.get('geomField')){
var value = doc.get(this.state.get('geomField'));
if (typeof(value) === 'string'){
// We *may* have a GeoJSON string representation
try {
return $.parseJSON(value);
value = $.parseJSON(value);
} catch(e) {
}
} else {
// We assume that the contents of the field are a valid GeoJSON object
return value;
}
if (value && value.lat) {
// not yet geojson so convert
value = {
"type": "Point",
"coordinates": [value.lon || value.lng, value.lat]
};
}
// We now 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'));

View File

@ -19,9 +19,9 @@ my.SlickGrid = Backbone.View.extend({
var self = this;
this.el = $(this.el);
_.bindAll(this, 'render');
this.model.currentDocuments.bind('add', this.render);
this.model.currentDocuments.bind('reset', this.render);
this.model.currentDocuments.bind('remove', this.render);
this.model.currentRecords.bind('add', this.render);
this.model.currentRecords.bind('reset', this.render);
this.model.currentRecords.bind('remove', this.render);
var state = _.extend({
hiddenColumns: [],
@ -110,7 +110,7 @@ my.SlickGrid = Backbone.View.extend({
var data = [];
this.model.currentDocuments.each(function(doc){
this.model.currentRecords.each(function(doc){
var row = {};
self.model.fields.each(function(field){
row[field.id] = doc.getFieldValue(field);

View File

@ -31,7 +31,7 @@ my.Timeline = Backbone.View.extend({
this.model.fields.bind('reset', function() {
self._setupTemporalField();
});
this.model.currentDocuments.bind('all', function() {
this.model.currentRecords.bind('all', function() {
self.reloadData();
});
var stateData = _.extend({
@ -78,11 +78,11 @@ my.Timeline = Backbone.View.extend({
]
}
};
this.model.currentDocuments.each(function(doc) {
this.model.currentRecords.each(function(doc) {
var start = doc.get(self.state.get('startField'));
if (start) {
var end = moment(doc.get(self.state.get('endField')));
end = end ? end.toDate() : null;
var end = doc.get(self.state.get('endField'));
end = end ? moment(end).toDate() : null;
var tlEntry = {
"startDate": moment(start).toDate(),
"endDate": end,

View File

@ -98,7 +98,7 @@ my.ColumnTransform = Backbone.View.extend({
}
this.el.modal('hide');
this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
var docs = self.model.currentDocuments.map(function(doc) {
var docs = self.model.currentRecords.map(function(doc) {
return doc.toJSON();
});
// TODO: notify about failed docs?
@ -107,14 +107,14 @@ my.ColumnTransform = Backbone.View.extend({
function onCompletedUpdate() {
totalToUpdate += -1;
if (totalToUpdate === 0) {
self.trigger('recline:flash', {message: toUpdate.length + " documents updated successfully"});
self.trigger('recline:flash', {message: toUpdate.length + " records updated successfully"});
alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)');
self.remove();
}
}
// TODO: Very inefficient as we search through all docs every time!
_.each(toUpdate, function(editedDoc) {
var realDoc = self.model.currentDocuments.get(editedDoc.id);
var realDoc = self.model.currentRecords.get(editedDoc.id);
realDoc.set(editedDoc);
realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate);
});
@ -158,7 +158,7 @@ my.ColumnTransform = Backbone.View.extend({
var editFunc = costco.evalFunction(e.target.value);
if (!editFunc.errorMessage) {
errors.text('No syntax error.');
var docs = self.model.currentDocuments.map(function(doc) {
var docs = self.model.currentRecords.map(function(doc) {
return doc.toJSON();
});
var previewData = costco.previewTransform(docs, editFunc, self.state.currentColumn);

View File

@ -1,695 +0,0 @@
/*jshint multistr:true */
// # Recline Views
//
// Recline Views are instances of Backbone Views and they act as 'WUI' (web
// user interface) component displaying some model object in the DOM. Like all
// Backbone views they have a pointer to a model (or a collection) and have an
// associated DOM-style element (usually this element will be bound into the
// page at some point).
//
// Views provided by core Recline are crudely divided into two types:
//
// * Dataset Views: a View intended for displaying a recline.Model.Dataset
// in some fashion. Examples are the Grid, Graph and Map views.
// * Widget Views: a widget used for displaying some specific (and
// smaller) aspect of a dataset or the application. Examples are
// QueryEditor and FilterEditor which both provide a way for editing (a
// part of) a `recline.Model.Query` associated to a Dataset.
//
// ## Dataset View
//
// These views are just Backbone views with a few additional conventions:
//
// 1. The model passed to the View should always be a recline.Model.Dataset instance
// 2. Views should generate their own root element rather than having it passed
// in.
// 3. Views should apply a css class named 'recline-{view-name-lower-cased} to
// the root element (and for all CSS for this view to be qualified using this
// CSS class)
// 4. Read-only mode: CSS for this view should respect/utilize
// recline-read-only class to trigger read-only behaviour (this class will
// usually be set on some parent element of the view's root element.
// 5. State: state (configuration) information for the view should be stored on
// an attribute named state that is an instance of a Backbone Model (or, more
// speficially, be an instance of `recline.Model.ObjectState`). In addition,
// a state attribute may be specified in the Hash passed to a View on
// iniitialization and this information should be used to set the initial
// state of the view.
//
// Example of state would be the set of fields being plotted in a graph
// view.
//
// More information about State can be found below.
//
// To summarize some of this, the initialize function for a Dataset View should
// look like:
//
// <pre>
// initialize: {
// model: {a recline.Model.Dataset instance}
// // el: {do not specify - instead view should create}
// state: {(optional) Object / Hash specifying initial state}
// ...
// }
// </pre>
//
// Note: Dataset Views in core Recline have a common layout on disk as
// follows, where ViewName is the named of View class:
//
// <pre>
// src/view-{lower-case-ViewName}.js
// css/{lower-case-ViewName}.css
// test/view-{lower-case-ViewName}.js
// </pre>
//
// ### State
//
// State information exists in order to support state serialization into the
// url or elsewhere and reloading of application from a stored state.
//
// State is available not only for individual views (as described above) but
// for the dataset (e.g. the current query). For an example of pulling together
// state from across multiple components see `recline.View.DataExplorer`.
//
// ### Flash Messages / Notifications
//
// To send 'flash messages' or notifications the convention is that views
// should fire an event named `recline:flash` with a payload that is a
// flash object with the following attributes (all optional):
//
// * message: message to show.
// * category: warning (default), success, error
// * persist: if true alert is persistent, o/w hidden after 3s (default=false)
// * loader: if true show a loading message
//
// Objects or views wishing to bind to flash messages may then subscribe to
// these events and take some action such as displaying them to the user. For
// an example of such behaviour see the DataExplorer view.
//
// ### Writing your own Views
//
// See the existing Views.
//
// ----
// Standard JS module setup
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
// ## DataExplorer
//
// The primary view for the entire application. Usage:
//
// <pre>
// var myExplorer = new model.recline.DataExplorer({
// model: {{recline.Model.Dataset instance}}
// el: {{an existing dom element}}
// views: {{dataset views}}
// state: {{state configuration -- see below}}
// });
// </pre>
//
// ### Parameters
//
// **model**: (required) recline.model.Dataset instance.
//
// **el**: (required) DOM element to bind to. NB: the element already
// being in the DOM is important for rendering of some subviews (e.g.
// Graph).
//
// **views**: (optional) the dataset views (Grid, Graph etc) for
// DataExplorer to show. This is an array of view hashes. If not provided
// initialize with (recline.View.)Grid, Graph, and Map views (with obvious id
// and labels!).
//
// <pre>
// var views = [
// {
// id: 'grid', // used for routing
// label: 'Grid', // used for view switcher
// view: new recline.View.Grid({
// model: dataset
// })
// },
// {
// id: 'graph',
// label: 'Graph',
// view: new recline.View.Graph({
// model: dataset
// })
// }
// ];
// </pre>
//
// **state**: standard state config for this view. This state is slightly
// special as it includes config of many of the subviews.
//
// <pre>
// state = {
// query: {dataset query state - see dataset.queryState object}
// view-{id1}: {view-state for this view}
// view-{id2}: {view-state for }
// ...
// // Explorer
// currentView: id of current view (defaults to first view if not specified)
// readOnly: (default: false) run in read-only mode
// }
// </pre>
//
// Note that at present we do *not* serialize information about the actual set
// of views in use -- e.g. those specified by the views argument -- but instead
// expect either that the default views are fine or that the client to have
// initialized the DataExplorer with the relevant views themselves.
my.DataExplorer = Backbone.View.extend({
template: ' \
<div class="recline-data-explorer"> \
<div class="alert-messages"></div> \
\
<div class="header"> \
<ul class="navigation"> \
{{#views}} \
<li><a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
{{/views}} \
</ul> \
<div class="recline-results-info"> \
Results found <span class="doc-count">{{docCount}}</span> \
</div> \
<div class="menu-right"> \
<a href="#" class="btn" data-action="filters">Filters</a> \
<a href="#" class="btn" data-action="facets">Facets</a> \
</div> \
<div class="query-editor-here" style="display:inline;"></div> \
<div class="clearfix"></div> \
</div> \
<div class="data-view-container"></div> \
</div> \
',
events: {
'click .menu-right a': '_onMenuClick',
'click .navigation a': '_onSwitchView'
},
initialize: function(options) {
var self = this;
this.el = $(this.el);
this._setupState(options.state);
// Hash of 'page' views (i.e. those for whole page) keyed by page name
if (options.views) {
this.pageViews = options.views;
} else {
this.pageViews = [{
id: 'grid',
label: 'Grid',
view: new my.Grid({
model: this.model,
state: this.state.get('view-grid')
}),
}, {
id: 'graph',
label: 'Graph',
view: new my.Graph({
model: this.model,
state: this.state.get('view-graph')
}),
}, {
id: 'map',
label: 'Map',
view: new my.Map({
model: this.model,
state: this.state.get('view-map')
}),
}, {
id: 'timeline',
label: 'Timeline',
view: new my.Timeline({
model: this.model,
state: this.state.get('view-timeline')
}),
}];
}
// these must be called after pageViews are created
this.render();
this._bindStateChanges();
this._bindFlashNotifications();
// now do updates based on state (need to come after render)
if (this.state.get('readOnly')) {
this.setReadOnly();
}
if (this.state.get('currentView')) {
this.updateNav(this.state.get('currentView'));
} else {
this.updateNav(this.pageViews[0].id);
}
this.model.bind('query:start', function() {
self.notify({loader: true, persist: true});
});
this.model.bind('query:done', function() {
self.clearNotifications();
self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
});
this.model.bind('query:fail', function(error) {
self.clearNotifications();
var msg = '';
if (typeof(error) == 'string') {
msg = error;
} else if (typeof(error) == 'object') {
if (error.title) {
msg = error.title + ': ';
}
if (error.message) {
msg += error.message;
}
} else {
msg = 'There was an error querying the backend';
}
self.notify({message: msg, category: 'error', persist: true});
});
// retrieve basic data like fields etc
// note this.model and dataset returned are the same
this.model.fetch()
.done(function(dataset) {
self.model.query(self.state.get('query'));
})
.fail(function(error) {
self.notify({message: error.message, category: 'error', persist: true});
});
},
setReadOnly: function() {
this.el.addClass('recline-read-only');
},
render: function() {
var tmplData = this.model.toTemplateJSON();
tmplData.views = this.pageViews;
var template = Mustache.render(this.template, tmplData);
$(this.el).html(template);
var $dataViewContainer = this.el.find('.data-view-container');
_.each(this.pageViews, function(view, pageName) {
$dataViewContainer.append(view.view.el);
});
var queryEditor = new my.QueryEditor({
model: this.model.queryState
});
this.el.find('.query-editor-here').append(queryEditor.el);
var filterEditor = new my.FilterEditor({
model: this.model.queryState
});
this.$filterEditor = filterEditor.el;
this.el.find('.header').append(filterEditor.el);
var facetViewer = new my.FacetViewer({
model: this.model
});
this.$facetViewer = facetViewer.el;
this.el.find('.header').append(facetViewer.el);
},
updateNav: function(pageName) {
this.el.find('.navigation li').removeClass('active');
this.el.find('.navigation li a').removeClass('disabled');
var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]');
$el.parent().addClass('active');
$el.addClass('disabled');
// show the specific page
_.each(this.pageViews, function(view, idx) {
if (view.id === pageName) {
view.view.el.show();
view.view.trigger('view:show');
} else {
view.view.el.hide();
view.view.trigger('view:hide');
}
});
},
_onMenuClick: function(e) {
e.preventDefault();
var action = $(e.target).attr('data-action');
if (action === 'filters') {
this.$filterEditor.show();
} else if (action === 'facets') {
this.$facetViewer.show();
}
},
_onSwitchView: function(e) {
e.preventDefault();
var viewName = $(e.target).attr('data-view');
this.updateNav(viewName);
this.state.set({currentView: viewName});
},
// create a state object for this view and do the job of
//
// a) initializing it from both data passed in and other sources (e.g. hash url)
//
// b) ensure the state object is updated in responese to changes in subviews, query etc.
_setupState: function(initialState) {
var self = this;
// get data from the query string / hash url plus some defaults
var qs = recline.Util.parseHashQueryString();
var query = qs.reclineQuery;
query = query ? JSON.parse(query) : self.model.queryState.toJSON();
// backwards compatability (now named view-graph but was named graph)
var graphState = qs['view-graph'] || qs.graph;
graphState = graphState ? JSON.parse(graphState) : {};
// now get default data + hash url plus initial state and initial our state object with it
var stateData = _.extend({
query: query,
'view-graph': graphState,
backend: this.model.backend.__type__,
url: this.model.get('url'),
currentView: null,
readOnly: false
},
initialState);
this.state = new recline.Model.ObjectState(stateData);
},
_bindStateChanges: function() {
var self = this;
// finally ensure we update our state object when state of sub-object changes so that state is always up to date
this.model.queryState.bind('change', function() {
self.state.set({query: self.model.queryState.toJSON()});
});
_.each(this.pageViews, function(pageView) {
if (pageView.view.state && pageView.view.state.bind) {
var update = {};
update['view-' + pageView.id] = pageView.view.state.toJSON();
self.state.set(update);
pageView.view.state.bind('change', function() {
var update = {};
update['view-' + pageView.id] = pageView.view.state.toJSON();
// had problems where change not being triggered for e.g. grid view so let's do it explicitly
self.state.set(update, {silent: true});
self.state.trigger('change');
});
}
});
},
_bindFlashNotifications: function() {
var self = this;
_.each(this.pageViews, function(pageView) {
pageView.view.bind('recline:flash', function(flash) {
self.notify(flash);
});
});
},
// ### notify
//
// Create a notification (a div.alert in div.alert-messsages) using provided
// flash object. Flash attributes (all are optional):
//
// * message: message to show.
// * category: warning (default), success, error
// * persist: if true alert is persistent, o/w hidden after 3s (default = false)
// * loader: if true show loading spinner
notify: function(flash) {
var tmplData = _.extend({
message: 'Loading',
category: 'warning',
loader: false
},
flash
);
if (tmplData.loader) {
var _template = ' \
<div class="alert alert-info alert-loader"> \
{{message}} \
<span class="notification-loader">&nbsp;</span> \
</div>';
} else {
var _template = ' \
<div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
{{message}} \
</div>';
}
var _templated = $(Mustache.render(_template, tmplData));
_templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
if (!flash.persist) {
setTimeout(function() {
$(_templated).fadeOut(1000, function() {
$(this).remove();
});
}, 1000);
}
},
// ### clearNotifications
//
// Clear all existing notifications
clearNotifications: function() {
var $notifications = $('.recline-data-explorer .alert-messages .alert');
$notifications.fadeOut(1500, function() {
$(this).remove();
});
}
});
// ### DataExplorer.restore
//
// Restore a DataExplorer instance from a serialized state including the associated dataset
my.DataExplorer.restore = function(state) {
var dataset = recline.Model.Dataset.restore(state);
var explorer = new my.DataExplorer({
model: dataset,
state: state
});
return explorer;
}
my.QueryEditor = Backbone.View.extend({
className: 'recline-query-editor',
template: ' \
<form action="" method="GET" class="form-inline"> \
<div class="input-prepend text-query"> \
<span class="add-on"><i class="icon-search"></i></span> \
<input type="text" name="q" value="{{q}}" class="span2" placeholder="Search data ..." class="search-query" /> \
</div> \
<div class="pagination"> \
<ul> \
<li class="prev action-pagination-update"><a href="">&laquo;</a></li> \
<li class="active"><a><input name="from" type="text" value="{{from}}" /> &ndash; <input name="to" type="text" value="{{to}}" /> </a></li> \
<li class="next action-pagination-update"><a href="">&raquo;</a></li> \
</ul> \
</div> \
<button type="submit" class="btn">Go &raquo;</button> \
</form> \
',
events: {
'submit form': 'onFormSubmit',
'click .action-pagination-update': 'onPaginationUpdate'
},
initialize: function() {
_.bindAll(this, 'render');
this.el = $(this.el);
this.model.bind('change', this.render);
this.render();
},
onFormSubmit: function(e) {
e.preventDefault();
var query = this.el.find('.text-query input').val();
var newFrom = parseInt(this.el.find('input[name="from"]').val());
var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
this.model.set({size: newSize, from: newFrom, q: query});
},
onPaginationUpdate: function(e) {
e.preventDefault();
var $el = $(e.target);
var newFrom = 0;
if ($el.parent().hasClass('prev')) {
newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
} else {
newFrom = this.model.get('from') + this.model.get('size');
}
this.model.set({from: newFrom});
},
render: function() {
var tmplData = this.model.toJSON();
tmplData.to = this.model.get('from') + this.model.get('size');
var templated = Mustache.render(this.template, tmplData);
this.el.html(templated);
}
});
my.FilterEditor = Backbone.View.extend({
className: 'recline-filter-editor well',
template: ' \
<a class="close js-hide" href="#">&times;</a> \
<div class="row filters"> \
<div class="span1"> \
<h3>Filters</h3> \
</div> \
<div class="span11"> \
<form class="form-horizontal"> \
<div class="row"> \
<div class="span6"> \
{{#termFilters}} \
<div class="control-group filter-term filter" data-filter-id={{id}}> \
<label class="control-label" for="">{{label}}</label> \
<div class="controls"> \
<div class="input-append"> \
<input type="text" value="{{value}}" name="{{fieldId}}" class="span4" data-filter-field="{{fieldId}}" data-filter-id="{{id}}" data-filter-type="term" /> \
<a class="btn js-remove-filter"><i class="icon-remove"></i></a> \
</div> \
</div> \
</div> \
{{/termFilters}} \
</div> \
<div class="span4"> \
<p>To add a filter use the column menu in the grid view.</p> \
<button type="submit" class="btn">Update</button> \
</div> \
</form> \
</div> \
</div> \
',
events: {
'click .js-hide': 'onHide',
'click .js-remove-filter': 'onRemoveFilter',
'submit form': 'onTermFiltersUpdate'
},
initialize: function() {
this.el = $(this.el);
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.model.bind('change:filters:new-blank', this.render);
this.render();
},
render: function() {
var tmplData = $.extend(true, {}, this.model.toJSON());
// we will use idx in list as there id ...
tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
filter.id = idx;
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]
};
});
var out = Mustache.render(this.template, tmplData);
this.el.html(out);
// are there actually any facets to show?
if (this.model.get('filters').length > 0) {
this.el.show();
} else {
this.el.hide();
}
},
onHide: function(e) {
e.preventDefault();
this.el.hide();
},
onRemoveFilter: function(e) {
e.preventDefault();
var $target = $(e.target);
var filterId = $target.closest('.filter').attr('data-filter-id');
this.model.removeFilter(filterId);
},
onTermFiltersUpdate: function(e) {
var self = this;
e.preventDefault();
var filters = self.model.get('filters');
var $form = $(e.target);
_.each($form.find('input'), function(input) {
var $input = $(input);
var filterIndex = parseInt($input.attr('data-filter-id'));
var value = $input.val();
var fieldId = $input.attr('data-filter-field');
filters[filterIndex].term[fieldId] = value;
});
self.model.set({filters: filters});
self.model.trigger('change');
}
});
my.FacetViewer = Backbone.View.extend({
className: 'recline-facet-viewer well',
template: ' \
<a class="close js-hide" href="#">&times;</a> \
<div class="facets row"> \
<div class="span1"> \
<h3>Facets</h3> \
</div> \
{{#facets}} \
<div class="facet-summary span2 dropdown" data-facet="{{id}}"> \
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-chevron-down"></i> {{id}} {{label}}</a> \
<ul class="facet-items dropdown-menu"> \
{{#terms}} \
<li><a class="facet-choice js-facet-filter" data-value="{{term}}">{{term}} ({{count}})</a></li> \
{{/terms}} \
{{#entries}} \
<li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
{{/entries}} \
</ul> \
</div> \
{{/facets}} \
</div> \
',
events: {
'click .js-hide': 'onHide',
'click .js-facet-filter': 'onFacetFilter'
},
initialize: function(model) {
_.bindAll(this, 'render');
this.el = $(this.el);
this.model.facets.bind('all', this.render);
this.model.fields.bind('all', this.render);
this.render();
},
render: function() {
var tmplData = {
facets: this.model.facets.toJSON(),
fields: this.model.fields.toJSON()
};
tmplData.facets = _.map(tmplData.facets, function(facet) {
if (facet._type === 'date_histogram') {
facet.entries = _.map(facet.entries, function(entry) {
entry.term = new Date(entry.time).toDateString();
return entry;
});
}
return facet;
});
var templated = Mustache.render(this.template, tmplData);
this.el.html(templated);
// are there actually any facets to show?
if (this.model.facets.length > 0) {
this.el.show();
} else {
this.el.hide();
}
},
onHide: function(e) {
e.preventDefault();
this.el.hide();
},
onFacetFilter: function(e) {
var $target= $(e.target);
var fieldId = $target.closest('.facet-summary').attr('data-facet');
var value = $target.attr('data-value');
this.model.queryState.addTermFilter(fieldId, value);
}
});
})(jQuery, recline.View);

452
src/view.multiview.js Normal file
View File

@ -0,0 +1,452 @@
/*jshint multistr:true */
// Standard JS module setup
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
// ## MultiView
//
// Manage multiple views together along with query editor etc. Usage:
//
// <pre>
// var myExplorer = new model.recline.MultiView({
// model: {{recline.Model.Dataset instance}}
// el: {{an existing dom element}}
// views: {{dataset views}}
// state: {{state configuration -- see below}}
// });
// </pre>
//
// ### Parameters
//
// **model**: (required) recline.model.Dataset instance.
//
// **el**: (required) DOM element to bind to. NB: the element already
// being in the DOM is important for rendering of some subviews (e.g.
// Graph).
//
// **views**: (optional) the dataset views (Grid, Graph etc) for
// MultiView to show. This is an array of view hashes. If not provided
// initialize with (recline.View.)Grid, Graph, and Map views (with obvious id
// and labels!).
//
// <pre>
// var views = [
// {
// id: 'grid', // used for routing
// label: 'Grid', // used for view switcher
// view: new recline.View.Grid({
// model: dataset
// })
// },
// {
// id: 'graph',
// label: 'Graph',
// view: new recline.View.Graph({
// model: dataset
// })
// }
// ];
// </pre>
//
// **state**: standard state config for this view. This state is slightly
// special as it includes config of many of the subviews.
//
// <pre>
// state = {
// query: {dataset query state - see dataset.queryState object}
// view-{id1}: {view-state for this view}
// view-{id2}: {view-state for }
// ...
// // Explorer
// currentView: id of current view (defaults to first view if not specified)
// readOnly: (default: false) run in read-only mode
// }
// </pre>
//
// Note that at present we do *not* serialize information about the actual set
// of views in use -- e.g. those specified by the views argument -- but instead
// expect either that the default views are fine or that the client to have
// initialized the MultiView with the relevant views themselves.
my.MultiView = Backbone.View.extend({
template: ' \
<div class="recline-data-explorer"> \
<div class="alert-messages"></div> \
\
<div class="header"> \
<ul class="navigation"> \
{{#views}} \
<li><a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
{{/views}} \
</ul> \
<div class="recline-results-info"> \
Results found <span class="doc-count">{{docCount}}</span> \
</div> \
<div class="menu-right"> \
<a href="#" class="btn" data-action="filters">Filters</a> \
<a href="#" class="btn" data-action="facets">Facets</a> \
</div> \
<div class="query-editor-here" style="display:inline;"></div> \
<div class="clearfix"></div> \
</div> \
<div class="data-view-container"></div> \
</div> \
',
events: {
'click .menu-right a': '_onMenuClick',
'click .navigation a': '_onSwitchView'
},
initialize: function(options) {
var self = this;
this.el = $(this.el);
this._setupState(options.state);
// Hash of 'page' views (i.e. those for whole page) keyed by page name
if (options.views) {
this.pageViews = options.views;
} else {
this.pageViews = [{
id: 'grid',
label: 'Grid',
view: new my.Grid({
model: this.model,
state: this.state.get('view-grid')
}),
}, {
id: 'graph',
label: 'Graph',
view: new my.Graph({
model: this.model,
state: this.state.get('view-graph')
}),
}, {
id: 'map',
label: 'Map',
view: new my.Map({
model: this.model,
state: this.state.get('view-map')
}),
}, {
id: 'timeline',
label: 'Timeline',
view: new my.Timeline({
model: this.model,
state: this.state.get('view-timeline')
}),
}];
}
// these must be called after pageViews are created
this.render();
this._bindStateChanges();
this._bindFlashNotifications();
// now do updates based on state (need to come after render)
if (this.state.get('readOnly')) {
this.setReadOnly();
}
if (this.state.get('currentView')) {
this.updateNav(this.state.get('currentView'));
} else {
this.updateNav(this.pageViews[0].id);
}
this.model.bind('query:start', function() {
self.notify({loader: true, persist: true});
});
this.model.bind('query:done', function() {
self.clearNotifications();
self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
});
this.model.bind('query:fail', function(error) {
self.clearNotifications();
var msg = '';
if (typeof(error) == 'string') {
msg = error;
} else if (typeof(error) == 'object') {
if (error.title) {
msg = error.title + ': ';
}
if (error.message) {
msg += error.message;
}
} else {
msg = 'There was an error querying the backend';
}
self.notify({message: msg, category: 'error', persist: true});
});
// retrieve basic data like fields etc
// note this.model and dataset returned are the same
this.model.fetch()
.done(function(dataset) {
self.model.query(self.state.get('query'));
})
.fail(function(error) {
self.notify({message: error.message, category: 'error', persist: true});
});
},
setReadOnly: function() {
this.el.addClass('recline-read-only');
},
render: function() {
var tmplData = this.model.toTemplateJSON();
tmplData.views = this.pageViews;
var template = Mustache.render(this.template, tmplData);
$(this.el).html(template);
var $dataViewContainer = this.el.find('.data-view-container');
_.each(this.pageViews, function(view, pageName) {
$dataViewContainer.append(view.view.el);
});
var pager = new recline.View.Pager({
model: this.model.queryState
});
this.el.find('.recline-results-info').after(pager.el);
var queryEditor = new recline.View.QueryEditor({
model: this.model.queryState
});
this.el.find('.query-editor-here').append(queryEditor.el);
var filterEditor = new recline.View.FilterEditor({
model: this.model.queryState
});
this.$filterEditor = filterEditor.el;
this.el.find('.header').append(filterEditor.el);
var facetViewer = new recline.View.FacetViewer({
model: this.model
});
this.$facetViewer = facetViewer.el;
this.el.find('.header').append(facetViewer.el);
},
updateNav: function(pageName) {
this.el.find('.navigation li').removeClass('active');
this.el.find('.navigation li a').removeClass('disabled');
var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]');
$el.parent().addClass('active');
$el.addClass('disabled');
// show the specific page
_.each(this.pageViews, function(view, idx) {
if (view.id === pageName) {
view.view.el.show();
view.view.trigger('view:show');
} else {
view.view.el.hide();
view.view.trigger('view:hide');
}
});
},
_onMenuClick: function(e) {
e.preventDefault();
var action = $(e.target).attr('data-action');
if (action === 'filters') {
this.$filterEditor.show();
} else if (action === 'facets') {
this.$facetViewer.show();
}
},
_onSwitchView: function(e) {
e.preventDefault();
var viewName = $(e.target).attr('data-view');
this.updateNav(viewName);
this.state.set({currentView: viewName});
},
// create a state object for this view and do the job of
//
// a) initializing it from both data passed in and other sources (e.g. hash url)
//
// b) ensure the state object is updated in responese to changes in subviews, query etc.
_setupState: function(initialState) {
var self = this;
// get data from the query string / hash url plus some defaults
var qs = my.parseHashQueryString();
var query = qs.reclineQuery;
query = query ? JSON.parse(query) : self.model.queryState.toJSON();
// backwards compatability (now named view-graph but was named graph)
var graphState = qs['view-graph'] || qs.graph;
graphState = graphState ? JSON.parse(graphState) : {};
// now get default data + hash url plus initial state and initial our state object with it
var stateData = _.extend({
query: query,
'view-graph': graphState,
backend: this.model.backend.__type__,
url: this.model.get('url'),
currentView: null,
readOnly: false
},
initialState);
this.state = new recline.Model.ObjectState(stateData);
},
_bindStateChanges: function() {
var self = this;
// finally ensure we update our state object when state of sub-object changes so that state is always up to date
this.model.queryState.bind('change', function() {
self.state.set({query: self.model.queryState.toJSON()});
});
_.each(this.pageViews, function(pageView) {
if (pageView.view.state && pageView.view.state.bind) {
var update = {};
update['view-' + pageView.id] = pageView.view.state.toJSON();
self.state.set(update);
pageView.view.state.bind('change', function() {
var update = {};
update['view-' + pageView.id] = pageView.view.state.toJSON();
// had problems where change not being triggered for e.g. grid view so let's do it explicitly
self.state.set(update, {silent: true});
self.state.trigger('change');
});
}
});
},
_bindFlashNotifications: function() {
var self = this;
_.each(this.pageViews, function(pageView) {
pageView.view.bind('recline:flash', function(flash) {
self.notify(flash);
});
});
},
// ### notify
//
// Create a notification (a div.alert in div.alert-messsages) using provided
// flash object. Flash attributes (all are optional):
//
// * message: message to show.
// * category: warning (default), success, error
// * persist: if true alert is persistent, o/w hidden after 3s (default = false)
// * loader: if true show loading spinner
notify: function(flash) {
var tmplData = _.extend({
message: 'Loading',
category: 'warning',
loader: false
},
flash
);
if (tmplData.loader) {
var _template = ' \
<div class="alert alert-info alert-loader"> \
{{message}} \
<span class="notification-loader">&nbsp;</span> \
</div>';
} else {
var _template = ' \
<div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
{{message}} \
</div>';
}
var _templated = $(Mustache.render(_template, tmplData));
_templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
if (!flash.persist) {
setTimeout(function() {
$(_templated).fadeOut(1000, function() {
$(this).remove();
});
}, 1000);
}
},
// ### clearNotifications
//
// Clear all existing notifications
clearNotifications: function() {
var $notifications = $('.recline-data-explorer .alert-messages .alert');
$notifications.fadeOut(1500, function() {
$(this).remove();
});
}
});
// ### MultiView.restore
//
// Restore a MultiView instance from a serialized state including the associated dataset
my.MultiView.restore = function(state) {
var dataset = recline.Model.Dataset.restore(state);
var explorer = new my.MultiView({
model: dataset,
state: state
});
return explorer;
}
// ## Miscellaneous Utilities
var urlPathRegex = /^([^?]+)(\?.*)?/;
// Parse the Hash section of a URL into path and query string
my.parseHashUrl = function(hashUrl) {
var parsed = urlPathRegex.exec(hashUrl);
if (parsed === null) {
return {};
} else {
return {
path: parsed[1],
query: parsed[2] || ''
};
}
};
// Parse a URL query string (?xyz=abc...) into a dictionary.
my.parseQueryString = function(q) {
if (!q) {
return {};
}
var urlParams = {},
e, d = function (s) {
return unescape(s.replace(/\+/g, " "));
},
r = /([^&=]+)=?([^&]*)/g;
if (q && q.length && q[0] === '?') {
q = q.slice(1);
}
while (e = r.exec(q)) {
// TODO: have values be array as query string allow repetition of keys
urlParams[d(e[1])] = d(e[2]);
}
return urlParams;
};
// Parse the query string out of the URL hash
my.parseHashQueryString = function() {
q = my.parseHashUrl(window.location.hash).query;
return my.parseQueryString(q);
};
// Compse a Query String
my.composeQueryString = function(queryParams) {
var queryString = '?';
var items = [];
$.each(queryParams, function(key, value) {
if (typeof(value) === 'object') {
value = JSON.stringify(value);
}
items.push(key + '=' + encodeURIComponent(value));
});
queryString += items.join('&');
return queryString;
};
my.getNewHashForQueryString = function(queryParams) {
var queryPart = my.composeQueryString(queryParams);
if (window.location.hash) {
// slice(1) to remove # at start
return window.location.hash.split('?')[0].slice(1) + queryPart;
} else {
return queryPart;
}
};
my.setHashQueryString = function(queryParams) {
window.location.hash = my.getNewHashForQueryString(queryParams);
};
})(jQuery, recline.View);

80
src/widget.facetviewer.js Normal file
View File

@ -0,0 +1,80 @@
/*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
my.FacetViewer = Backbone.View.extend({
className: 'recline-facet-viewer well',
template: ' \
<a class="close js-hide" href="#">&times;</a> \
<div class="facets row"> \
<div class="span1"> \
<h3>Facets</h3> \
</div> \
{{#facets}} \
<div class="facet-summary span2 dropdown" data-facet="{{id}}"> \
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-chevron-down"></i> {{id}} {{label}}</a> \
<ul class="facet-items dropdown-menu"> \
{{#terms}} \
<li><a class="facet-choice js-facet-filter" data-value="{{term}}">{{term}} ({{count}})</a></li> \
{{/terms}} \
{{#entries}} \
<li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
{{/entries}} \
</ul> \
</div> \
{{/facets}} \
</div> \
',
events: {
'click .js-hide': 'onHide',
'click .js-facet-filter': 'onFacetFilter'
},
initialize: function(model) {
_.bindAll(this, 'render');
this.el = $(this.el);
this.model.facets.bind('all', this.render);
this.model.fields.bind('all', this.render);
this.render();
},
render: function() {
var tmplData = {
facets: this.model.facets.toJSON(),
fields: this.model.fields.toJSON()
};
tmplData.facets = _.map(tmplData.facets, function(facet) {
if (facet._type === 'date_histogram') {
facet.entries = _.map(facet.entries, function(entry) {
entry.term = new Date(entry.time).toDateString();
return entry;
});
}
return facet;
});
var templated = Mustache.render(this.template, tmplData);
this.el.html(templated);
// are there actually any facets to show?
if (this.model.facets.length > 0) {
this.el.show();
} else {
this.el.hide();
}
},
onHide: function(e) {
e.preventDefault();
this.el.hide();
},
onFacetFilter: function(e) {
var $target= $(e.target);
var fieldId = $target.closest('.facet-summary').attr('data-facet');
var value = $target.attr('data-value');
this.model.queryState.addTermFilter(fieldId, value);
}
});
})(jQuery, recline.View);

109
src/widget.filtereditor.js Normal file
View File

@ -0,0 +1,109 @@
/*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
my.FilterEditor = Backbone.View.extend({
className: 'recline-filter-editor well',
template: ' \
<a class="close js-hide" href="#">&times;</a> \
<div class="row filters"> \
<div class="span1"> \
<h3>Filters</h3> \
</div> \
<div class="span11"> \
<form class="form-horizontal"> \
<div class="row"> \
<div class="span6"> \
{{#termFilters}} \
<div class="control-group filter-term filter" data-filter-id={{id}}> \
<label class="control-label" for="">{{label}}</label> \
<div class="controls"> \
<div class="input-append"> \
<input type="text" value="{{value}}" name="{{fieldId}}" class="span4" data-filter-field="{{fieldId}}" data-filter-id="{{id}}" data-filter-type="term" /> \
<a class="btn js-remove-filter"><i class="icon-remove"></i></a> \
</div> \
</div> \
</div> \
{{/termFilters}} \
</div> \
<div class="span4"> \
<p>To add a filter use the column menu in the grid view.</p> \
<button type="submit" class="btn">Update</button> \
</div> \
</form> \
</div> \
</div> \
',
events: {
'click .js-hide': 'onHide',
'click .js-remove-filter': 'onRemoveFilter',
'submit form': 'onTermFiltersUpdate'
},
initialize: function() {
this.el = $(this.el);
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.model.bind('change:filters:new-blank', this.render);
this.render();
},
render: function() {
var tmplData = $.extend(true, {}, this.model.toJSON());
// we will use idx in list as there id ...
tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
filter.id = idx;
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]
};
});
var out = Mustache.render(this.template, tmplData);
this.el.html(out);
// are there actually any facets to show?
if (this.model.get('filters').length > 0) {
this.el.show();
} else {
this.el.hide();
}
},
onHide: function(e) {
e.preventDefault();
this.el.hide();
},
onRemoveFilter: function(e) {
e.preventDefault();
var $target = $(e.target);
var filterId = $target.closest('.filter').attr('data-filter-id');
this.model.removeFilter(filterId);
},
onTermFiltersUpdate: function(e) {
var self = this;
e.preventDefault();
var filters = self.model.get('filters');
var $form = $(e.target);
_.each($form.find('input'), function(input) {
var $input = $(input);
var filterIndex = parseInt($input.attr('data-filter-id'));
var value = $input.val();
var fieldId = $input.attr('data-filter-field');
filters[filterIndex].term[fieldId] = value;
});
self.model.set({filters: filters});
self.model.trigger('change');
}
});
})(jQuery, recline.View);

57
src/widget.pager.js Normal file
View File

@ -0,0 +1,57 @@
/*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
my.Pager = Backbone.View.extend({
className: 'recline-pager',
template: ' \
<div class="pagination"> \
<ul> \
<li class="prev action-pagination-update"><a href="">&laquo;</a></li> \
<li class="active"><a><input name="from" type="text" value="{{from}}" /> &ndash; <input name="to" type="text" value="{{to}}" /> </a></li> \
<li class="next action-pagination-update"><a href="">&raquo;</a></li> \
</ul> \
</div> \
',
events: {
'click .action-pagination-update': 'onPaginationUpdate',
'change input': 'onFormSubmit'
},
initialize: function() {
_.bindAll(this, 'render');
this.el = $(this.el);
this.model.bind('change', this.render);
this.render();
},
onFormSubmit: function(e) {
e.preventDefault();
var newFrom = parseInt(this.el.find('input[name="from"]').val());
var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
this.model.set({size: newSize, from: newFrom});
},
onPaginationUpdate: function(e) {
e.preventDefault();
var $el = $(e.target);
var newFrom = 0;
if ($el.parent().hasClass('prev')) {
newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
} else {
newFrom = this.model.get('from') + this.model.get('size');
}
this.model.set({from: newFrom});
},
render: function() {
var tmplData = this.model.toJSON();
tmplData.to = this.model.get('from') + this.model.get('size');
var templated = Mustache.render(this.template, tmplData);
this.el.html(templated);
}
});
})(jQuery, recline.View);

44
src/widget.queryeditor.js Normal file
View File

@ -0,0 +1,44 @@
/*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
my.QueryEditor = Backbone.View.extend({
className: 'recline-query-editor',
template: ' \
<form action="" method="GET" class="form-inline"> \
<div class="input-prepend text-query"> \
<span class="add-on"><i class="icon-search"></i></span> \
<input type="text" name="q" value="{{q}}" class="span2" placeholder="Search data ..." class="search-query" /> \
</div> \
<button type="submit" class="btn">Go &raquo;</button> \
</form> \
',
events: {
'submit form': 'onFormSubmit'
},
initialize: function() {
_.bindAll(this, 'render');
this.el = $(this.el);
this.model.bind('change', this.render);
this.render();
},
onFormSubmit: function(e) {
e.preventDefault();
var query = this.el.find('.text-query input').val();
this.model.set({q: query});
},
render: function() {
var tmplData = this.model.toJSON();
tmplData.to = this.model.get('from') + this.model.get('size');
var templated = Mustache.render(this.template, tmplData);
this.el.html(templated);
}
});
})(jQuery, recline.View);

View File

@ -155,11 +155,11 @@ test("write", function() {
stop();
var id = parseInt(Math.random()*100000000).toString();
var doc = {
var rec = {
id: id,
title: 'my title'
};
var jqxhr = backend.upsert(doc);
var jqxhr = backend.upsert(rec);
jqxhr.done(function(data) {
ok(data.ok);
equal(data._id, id);
@ -167,16 +167,16 @@ test("write", function() {
equal(data._version, 1);
// update
doc.title = 'new title';
var jqxhr = backend.upsert(doc);
rec.title = 'new title';
var jqxhr = backend.upsert(rec);
jqxhr.done(function(data) {
equal(data._version, 2);
// delete
var jqxhr = backend.delete(doc.id);
var jqxhr = backend.delete(rec.id);
jqxhr.done(function(data) {
ok(data.ok);
doc = null;
rec = null;
// try to get ...
var jqxhr = backend.get(id);
@ -233,10 +233,10 @@ test("query", function() {
dataset.fetch().done(function(dataset) {
deepEqual(['_created', '_last_modified', 'end', 'owner', 'start', 'title'], _.pluck(dataset.fields.toJSON(), 'id'));
dataset.query().then(function(docList) {
dataset.query().then(function(recList) {
equal(3, dataset.docCount);
equal(3, docList.length);
equal('Note 1', docList.models[0].get('title'));
equal(3, recList.length);
equal('Note 1', recList.models[0].get('title'));
start();
});
});
@ -254,14 +254,14 @@ test("write", function() {
stop();
var id = parseInt(Math.random()*100000000).toString();
var doc = new recline.Model.Document({
var rec = new recline.Model.Record({
id: id,
title: 'my title'
});
doc.backend = backend;
doc.dataset = dataset;
dataset.currentDocuments.add(doc);
var jqxhr = doc.save();
rec.backend = backend;
rec.dataset = dataset;
dataset.currentRecords.add(rec);
var jqxhr = rec.save();
jqxhr.done(function(data) {
ok(data.ok);
equal(data._id, id);
@ -269,29 +269,29 @@ test("write", function() {
equal(data._version, 1);
// update
doc.set({title: 'new title'});
var jqxhr = doc.save();
rec.set({title: 'new title'});
var jqxhr = rec.save();
jqxhr.done(function(data) {
equal(data._version, 2);
// delete
var jqxhr = doc.destroy();
var jqxhr = rec.destroy();
jqxhr.done(function(data) {
ok(data.ok);
doc = null;
rec = null;
// try to get ...
var olddoc = new recline.Model.Document({id: id});
equal(olddoc.get('title'), null);
olddoc.dataset = dataset;
olddoc.backend = backend;
var jqxhr = olddoc.fetch();
var oldrec = new recline.Model.Record({id: id});
equal(oldrec.get('title'), null);
oldrec.dataset = dataset;
oldrec.backend = backend;
var jqxhr = oldrec.fetch();
jqxhr.done(function(data) {
// should not be here
ok(false, 'Should have got 404');
}).error(function(error) {
equal(error.status, 404);
equal(typeof olddoc.get('title'), 'undefined');
equal(typeof oldrec.get('title'), 'undefined');
start();
});
});

View File

@ -26,7 +26,7 @@ test("parseCSV", function() {
'"Other, AN", 12:35\n';
var dataset = recline.Backend.CSV.csvToDataset(csv);
dataset.query();
equal(dataset.currentDocuments.length, 3);
equal(dataset.currentRecords.length, 3);
});
test("parseCSVsemicolon", function() {

View File

@ -30,8 +30,8 @@ test('query', function () {
, from: 2
};
var out = data.query(queryObj);
deepEqual(out.documents[0], memoryData[2]);
equal(out.documents.length, 4);
deepEqual(out.records[0], memoryData[2]);
equal(out.records.length, 4);
equal(out.total, 6);
});
@ -43,18 +43,18 @@ test('query sort', function () {
]
};
var out = data.query(queryObj);
equal(out.documents[0].x, 6);
equal(out.records[0].x, 6);
});
test('query string', function () {
var data = _wrapData();
var out = data.query({q: 'UK'});
equal(out.total, 3);
deepEqual(_.pluck(out.documents, 'country'), ['UK', 'UK', 'UK']);
deepEqual(_.pluck(out.records, 'country'), ['UK', 'UK', 'UK']);
var out = data.query({q: 'UK 6'})
equal(out.total, 1);
deepEqual(out.documents[0].id, 1);
deepEqual(out.records[0].id, 1);
});
test('filters', function () {
@ -63,7 +63,7 @@ test('filters', function () {
query.addTermFilter('country', 'UK');
var out = data.query(query.toJSON());
equal(out.total, 3);
deepEqual(_.pluck(out.documents, 'country'), ['UK', 'UK', 'UK']);
deepEqual(_.pluck(out.records, 'country'), ['UK', 'UK', 'UK']);
});
test('facet', function () {
@ -118,7 +118,7 @@ var memoryData = {
, id: 'test-dataset'
},
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'}],
documents: [
records: [
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first'}
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
, {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
@ -129,16 +129,16 @@ var memoryData = {
};
function makeBackendDataset() {
var dataset = new recline.Backend.Memory.createDataset(memoryData.documents, null, memoryData.metadata);
var dataset = new recline.Backend.Memory.createDataset(memoryData.records, null, memoryData.metadata);
return dataset;
}
test('createDataset', function () {
var dataset = recline.Backend.Memory.createDataset(memoryData.documents);
var dataset = recline.Backend.Memory.createDataset(memoryData.records);
equal(dataset.fields.length, 6);
deepEqual(['id', 'x', 'y', 'z', 'country', 'label'], dataset.fields.pluck('id'));
dataset.query();
equal(memoryData.documents.length, dataset.currentDocuments.length);
equal(memoryData.records.length, dataset.currentRecords.length);
});
test('basics', function () {
@ -162,8 +162,8 @@ test('query', function () {
size: 4
, from: 2
};
dataset.query(queryObj).then(function(documentList) {
deepEqual(data[2], documentList.models[0].toJSON());
dataset.query(queryObj).then(function(recordList) {
deepEqual(data[2], recordList.models[0].toJSON());
});
});
@ -177,7 +177,7 @@ test('query sort', function () {
]
};
dataset.query(queryObj).then(function() {
var doc0 = dataset.currentDocuments.models[0].toJSON();
var doc0 = dataset.currentRecords.models[0].toJSON();
equal(doc0.x, 6);
});
});
@ -186,13 +186,13 @@ test('query string', function () {
var dataset = makeBackendDataset();
dataset.fetch();
dataset.query({q: 'UK'}).then(function() {
equal(dataset.currentDocuments.length, 3);
deepEqual(dataset.currentDocuments.pluck('country'), ['UK', 'UK', 'UK']);
equal(dataset.currentRecords.length, 3);
deepEqual(dataset.currentRecords.pluck('country'), ['UK', 'UK', 'UK']);
});
dataset.query({q: 'UK 6'}).then(function() {
equal(dataset.currentDocuments.length, 1);
deepEqual(dataset.currentDocuments.models[0].id, 1);
equal(dataset.currentRecords.length, 1);
deepEqual(dataset.currentRecords.models[0].id, 1);
});
});
@ -200,8 +200,8 @@ test('filters', function () {
var dataset = makeBackendDataset();
dataset.queryState.addTermFilter('country', 'UK');
dataset.query().then(function() {
equal(dataset.currentDocuments.length, 3);
deepEqual(dataset.currentDocuments.pluck('country'), ['UK', 'UK', 'UK']);
equal(dataset.currentRecords.length, 3);
deepEqual(dataset.currentRecords.pluck('country'), ['UK', 'UK', 'UK']);
});
});
@ -247,7 +247,7 @@ test('update and delete', function () {
// Test Delete
doc1.destroy().then(function() {
equal(data.data.length, 5);
equal(data.data[0].x, memoryData.documents[1].x);
equal(data.data[0].x, memoryData.records[1].x);
});
});
});

View File

@ -27,7 +27,6 @@
<script type="text/javascript" src="base.js"></script>
<script type="text/javascript" src="../src/util.js"></script>
<script type="text/javascript" src="../src/model.js"></script>
<script type="text/javascript" src="../src/backend/base.js"></script>
<script type="text/javascript" src="../src/backend/memory.js"></script>
@ -41,20 +40,24 @@
<script type="text/javascript" src="backend.elasticsearch.test.js"></script>
<script type="text/javascript" src="backend/csv.test.js"></script>
<script type="text/javascript" src="../src/view.js"></script>
<script type="text/javascript" src="../src/view-grid.js"></script>
<script type="text/javascript" src="../src/view-slickgrid.js"></script>
<script type="text/javascript" src="../src/view-transform-dialog.js"></script>
<script type="text/javascript" src="../src/view-graph.js"></script>
<script type="text/javascript" src="../src/view-map.js"></script>
<script type="text/javascript" src="../src/view-timeline.js"></script>
<script type="text/javascript" src="../src/widget.pager.js"></script>
<script type="text/javascript" src="../src/widget.queryeditor.js"></script>
<script type="text/javascript" src="../src/widget.filtereditor.js"></script>
<script type="text/javascript" src="../src/widget.facetviewer.js"></script>
<script type="text/javascript" src="../src/view.multiview.js"></script>
<script type="text/javascript" src="view-grid.test.js"></script>
<script type="text/javascript" src="view-slickgrid.test.js"></script>
<script type="text/javascript" src="view-graph.test.js"></script>
<script type="text/javascript" src="view-map.test.js"></script>
<script type="text/javascript" src="view-timeline.test.js"></script>
<script type="text/javascript" src="view.test.js"></script>
<script type="text/javascript" src="view.multiview.test.js"></script>
<script type="text/javascript" src="util.test.js"></script>
</head>
<body>

View File

@ -39,7 +39,7 @@ test('Field: basics', function () {
});
test('Field: default renderers', function () {
var doc = new recline.Model.Document({
var doc = new recline.Model.Record({
x: 12.3,
myobject: {a: 1, b: 2},
link: 'http://abc.com/',
@ -74,7 +74,7 @@ test('Field: default renderers', function () {
});
test('Field: custom deriver and renderer', function () {
var doc = new recline.Model.Document({x: 123});
var doc = new recline.Model.Record({x: 123});
var cellRenderer = function(value, field) {
return '<span class="field-' + field.id + '">' + value + '</span>';
}

View File

@ -2,10 +2,10 @@
module("Util");
test('parseHashUrl', function () {
var out = recline.Util.parseHashUrl('graph?x=y');
var out = recline.View.parseHashUrl('graph?x=y');
equal(out.path, 'graph');
equal(out.query, '?x=y');
var out = recline.Util.parseHashUrl('graph');
var out = recline.View.parseHashUrl('graph');
equal(out.path, 'graph');
equal(out.query, '');
});
@ -15,7 +15,7 @@ test('composeQueryString', function () {
x: 'y',
a: 'b'
};
var out = recline.Util.composeQueryString(params);
var out = recline.View.composeQueryString(params);
equal(out, '?x=y&a=b');
});

View File

@ -38,7 +38,7 @@ test('state', function () {
test('new GridRow View', function () {
var $el = $('<tr />');
$('.fixtures .test-datatable').append($el);
var doc = new recline.Model.Document({
var doc = new recline.Model.Record({
'id': 1,
'b': '2',
'a': '1'

View File

@ -11,12 +11,12 @@ var GeoJSONFixture = {
{id: 'z'},
{id: 'geom'}
];
var documents = [
var records = [
{id: 0, x: 1, y: 2, z: 3, geom: '{"type":"Point","coordinates":[13.40,52.35]}'},
{id: 1, x: 2, y: 4, z: 6, geom: {type:"Point",coordinates:[13.40,52.35]}},
{id: 2, x: 3, y: 6, z: 9, geom: {type:"LineString",coordinates:[[100.0, 0.0],[101.0, 1.0]]}}
];
var dataset = recline.Backend.Memory.createDataset(documents, fields);
var dataset = recline.Backend.Memory.createDataset(records, fields);
return dataset;
}
};
@ -55,12 +55,12 @@ test('Lat/Lon geom fields', function () {
// Check that all markers were created
equal(_getFeaturesCount(view.features),6);
// Delete a document
view.model.currentDocuments.remove(view.model.currentDocuments.get('1'));
// Delete a record
view.model.currentRecords.remove(view.model.currentRecords.get('1'));
equal(_getFeaturesCount(view.features),5);
// Add a new one
view.model.currentDocuments.add({id: 7, x: 7, y: 14, z: 21, country: 'KX', label: 'seventh', lat:13.23, lon:23.56}),
view.model.currentRecords.add({id: 7, x: 7, y: 14, z: 21, country: 'KX', label: 'seventh', lat:13.23, lon:23.56}),
equal(_getFeaturesCount(view.features),6);
view.remove();
@ -79,17 +79,34 @@ test('GeoJSON geom field', function () {
// Check that all features were created
equal(_getFeaturesCount(view.features),3);
// Delete a document
view.model.currentDocuments.remove(view.model.currentDocuments.get('2'));
// Delete a record
view.model.currentRecords.remove(view.model.currentRecords.get('2'));
equal(_getFeaturesCount(view.features),2);
// Add it back
view.model.currentDocuments.add({id: 2, x: 3, y: 6, z: 9, geom: {type:"LineString",coordinates:[[100.0, 0.0],[101.0, 1.0]]}}),
view.model.currentRecords.add({id: 2, x: 3, y: 6, z: 9, geom: {type:"LineString",coordinates:[[100.0, 0.0],[101.0, 1.0]]}}),
equal(_getFeaturesCount(view.features),3);
view.remove();
});
test('geom field non-GeoJSON', function () {
var data = [{
location: { lon: 47, lat: 53},
title: 'abc'
}];
var dataset = recline.Backend.Memory.createDataset(data);
var view = new recline.View.Map({
model: dataset
});
//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 () {
var dataset = GeoJSONFixture.getDataset();
var view = new recline.View.Map({

View File

@ -16,7 +16,7 @@ test('basic', function () {
assertPresent('.slick-header-column[title="x"]');
equal($('.slick-header-column').length,dataset.fields.length);
equal(dataset.currentDocuments.length,view.grid.getDataLength());
equal(dataset.currentRecords.length,view.grid.getDataLength());
view.remove();
});

View File

@ -17,16 +17,16 @@ test('extract dates and timelineJSON', function () {
'headline': '',
'date': [
{
'startDate': '2012-03-20',
'startDate': new Date('2012-03-20'),
'endDate': null,
'headline': '2012-03-20',
'text': ''
'headline': '1',
'text': '<div><strong>Date</strong>: 2012-03-20</div><div><strong>title</strong>: 1</div>'
},
{
'startDate': '2012-03-25',
'startDate': new Date('2012-03-25'),
'endDate': null,
'headline': '2012-03-25',
'text': ''
'headline': '2',
'text': '<div><strong>Date</strong>: 2012-03-25</div><div><strong>title</strong>: 2</div>'
}
]
}

View File

@ -6,7 +6,7 @@ test('basic explorer functionality', function () {
var $el = $('<div class="test-view-explorer-basic" />');
$('.fixtures .data-explorer-here').append($el);
var dataset = Fixture.getDataset();
var explorer = new recline.View.DataExplorer({
var explorer = new recline.View.MultiView({
model: dataset,
el: $el
});
@ -21,7 +21,7 @@ test('get State', function () {
var dataset = Fixture.getDataset();
var url = 'xyz';
dataset.set({url: url});
var explorer = new recline.View.DataExplorer({
var explorer = new recline.View.MultiView({
model: dataset,
el: $el
});
@ -41,7 +41,7 @@ test('initialize state', function () {
var $el = $('<div class="test-view-explorer-init-state" />');
$('.fixtures .data-explorer-here').append($el);
var dataset = Fixture.getDataset();
var explorer = new recline.View.DataExplorer({
var explorer = new recline.View.MultiView({
model: dataset,
el: $el,
state: {
@ -74,11 +74,11 @@ test('initialize state', function () {
test('restore (from serialized state)', function() {
var dataset = Fixture.getDataset();
var explorer = new recline.View.DataExplorer({
var explorer = new recline.View.MultiView({
model: dataset,
});
var state = explorer.state.toJSON();
var explorerNew = recline.View.DataExplorer.restore(state);
var explorerNew = recline.View.MultiView.restore(state);
var out = explorerNew.state.toJSON();
equal(out.backend, state.backend);
});