[ckan-changes] [okfn/ckan] 440d83: [2285] Update recline.js to latest version
GitHub
noreply at github.com
Wed Apr 25 13:27:11 UTC 2012
Branch: refs/heads/master
Home: https://github.com/okfn/ckan
Commit: 440d83093ece565ee55c1b454a5806d9bae2267a
https://github.com/okfn/ckan/commit/440d83093ece565ee55c1b454a5806d9bae2267a
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-20 (Fri, 20 Apr 2012)
Changed paths:
M ckan/public/scripts/vendor/recline/css/graph-flot.css
M ckan/public/scripts/vendor/recline/css/map.css
M ckan/public/scripts/vendor/recline/recline.js
Log Message:
-----------
[2285] Update recline.js to latest version
diff --git a/ckan/public/scripts/vendor/recline/css/graph-flot.css b/ckan/public/scripts/vendor/recline/css/graph-flot.css
index d50f11e..88acf5f 100644
--- a/ckan/public/scripts/vendor/recline/css/graph-flot.css
+++ b/ckan/public/scripts/vendor/recline/css/graph-flot.css
@@ -1,14 +1,14 @@
-.data-graph-container .graph {
+.recline-graph .graph {
height: 500px;
margin-right: 200px;
}
-.data-graph-container .legend table {
+.recline-graph .legend table {
width: auto;
margin-bottom: 0;
}
-.data-graph-container .legend td {
+.recline-graph .legend td {
padding: 5px;
line-height: 13px;
}
@@ -17,34 +17,34 @@
* Editor
*********************************************************/
-.data-graph-container .editor {
+.recline-graph .editor {
float: right;
width: 200px;
padding-left: 0px;
}
-.data-graph-container .editor-info {
+.recline-graph .editor-info {
padding-left: 4px;
}
-.data-graph-container .editor-info {
+.recline-graph .editor-info {
cursor: pointer;
}
-.data-graph-container .editor form {
+.recline-graph .editor form {
padding-left: 4px;
}
-.data-graph-container .editor select {
+.recline-graph .editor select {
width: 100%;
}
-.data-graph-container .editor-info {
+.recline-graph .editor-info {
border-bottom: 1px solid #ddd;
margin-bottom: 10px;
}
-.data-graph-container .editor-hide-info p {
+.recline-graph .editor-hide-info p {
display: none;
}
diff --git a/ckan/public/scripts/vendor/recline/css/map.css b/ckan/public/scripts/vendor/recline/css/map.css
index c8adde7..829d0c8 100644
--- a/ckan/public/scripts/vendor/recline/css/map.css
+++ b/ckan/public/scripts/vendor/recline/css/map.css
@@ -1,4 +1,4 @@
-.data-map-container .map {
+.recline-map .map {
height: 500px;
}
@@ -6,18 +6,18 @@
* Editor
*********************************************************/
-.data-map-container .editor {
+.recline-map .editor {
float: right;
width: 200px;
padding-left: 0px;
margin-left: 10px;
}
-.data-map-container .editor form {
+.recline-map .editor form {
padding-left: 4px;
}
-.data-map-container .editor select {
+.recline-map .editor select {
width: 100%;
}
diff --git a/ckan/public/scripts/vendor/recline/recline.js b/ckan/public/scripts/vendor/recline/recline.js
index 2871c6d..e404d78 100644
--- a/ckan/public/scripts/vendor/recline/recline.js
+++ b/ckan/public/scripts/vendor/recline/recline.js
@@ -86,7 +86,7 @@ this.recline.Model = this.recline.Model || {};
//
// @property {number} docCount: total number of documents in this dataset
//
-// @property {Backend} backend: the Backend (instance) for this Dataset
+// @property {Backend} backend: the Backend (instance) for this Dataset.
//
// @property {Query} queryState: `Query` object which stores current
// queryState. queryState may be edited by other components (e.g. a query
@@ -96,14 +96,24 @@ this.recline.Model = this.recline.Model || {};
// Facets.
my.Dataset = Backbone.Model.extend({
__type__: 'Dataset',
+
// ### initialize
//
// Sets up instance properties (see above)
+ //
+ // @param {Object} model: standard set of model attributes passed to Backbone models
+ //
+ // @param {Object or String} backend: Backend instance (see
+ // `recline.Backend.Base`) or a string specifying that instance. The
+ // string specifying may be a full class path e.g.
+ // 'recline.Backend.ElasticSearch' or a simple name e.g.
+ // 'elasticsearch' or 'ElasticSearch' (in this case must be a Backend in
+ // recline.Backend module)
initialize: function(model, backend) {
_.bindAll(this, 'query');
this.backend = backend;
- if (backend && backend.constructor == String) {
- this.backend = my.backends[backend];
+ if (typeof(backend) === 'string') {
+ this.backend = this._backendFromString(backend);
}
this.fields = new my.FieldList();
this.currentDocuments = new my.DocumentList();
@@ -167,9 +177,73 @@ my.Dataset = Backbone.Model.extend({
data.docCount = this.docCount;
data.fields = this.fields.toJSON();
return data;
+ },
+
+ // ### _backendFromString(backendString)
+ //
+ // See backend argument to initialize for details
+ _backendFromString: function(backendString) {
+ var parts = backendString.split('.');
+ // walk through the specified path xxx.yyy.zzz to get the final object which should be backend class
+ var current = window;
+ for(ii=0;ii<parts.length;ii++) {
+ if (!current) {
+ break;
+ }
+ current = current[parts[ii]];
+ }
+ if (current) {
+ return new current();
+ }
+
+ // alternatively we just had a simple string
+ var backend = null;
+ if (recline && recline.Backend) {
+ _.each(_.keys(recline.Backend), function(name) {
+ if (name.toLowerCase() === backendString.toLowerCase()) {
+ backend = new recline.Backend[name]();
+ }
+ });
+ }
+ return backend;
}
});
+
+// ### Dataset.restore
+//
+// Restore a Dataset instance from a serialized state. Serialized state for a
+// Dataset is an Object like:
+//
+// <pre>
+// {
+// backend: {backend type - i.e. value of dataset.backend.__type__}
+// dataset: {dataset info needed for loading -- result of dataset.toJSON() would be sufficient but can be simpler }
+// // convenience - if url provided and dataste not this be used as dataset url
+// url: {dataset url}
+// ...
+// }
+my.Dataset.restore = function(state) {
+ // hack-y - restoring a memory dataset does not mean much ...
+ var dataset = null;
+ if (state.url && !state.dataset) {
+ state.dataset = {url: state.url};
+ }
+ if (state.backend === 'memory') {
+ dataset = recline.Backend.createDataset(
+ [{stub: 'this is a stub dataset because we do not restore memory datasets'}],
+ [],
+ state.dataset // metadata
+ );
+ } else {
+ dataset = new recline.Model.Dataset(
+ state.dataset,
+ state.backend
+ );
+ }
+ return dataset;
+};
+
// ## <a id="document">A Document (aka Row)</a>
//
// A single entry or row in the dataset
@@ -449,6 +523,13 @@ my.FacetList = Backbone.Collection.extend({
model: my.Facet
});
+// ## Object State
+//
+// Convenience Backbone model for storing (configuration) state of objects like Views.
+my.ObjectState = Backbone.Model.extend({
+});
+
+
// ## Backend registry
//
// Backends will register themselves by id into this registry
@@ -618,10 +699,10 @@ this.recline.View = this.recline.View || {};
// ## Graph view for a Dataset using Flot graphing library.
//
-// Initialization arguments:
+// Initialization arguments (in a hash in first parameter):
//
// * model: recline.Model.Dataset
-// * config: (optional) graph configuration hash of form:
+// * state: (optional) configuration hash of form:
//
// {
// group: {column name for x-axis},
@@ -631,10 +712,10 @@ this.recline.View = this.recline.View || {};
//
// NB: should *not* provide an el argument to the view but must let the view
// generate the element itself (you can then append view.el to the DOM.
-my.FlotGraph = Backbone.View.extend({
+my.Graph = Backbone.View.extend({
tagName: "div",
- className: "data-graph-container",
+ className: "recline-graph",
template: ' \
<div class="editor"> \
@@ -697,7 +778,7 @@ my.FlotGraph = Backbone.View.extend({
'click .action-toggle-help': 'toggleHelp'
},
- initialize: function(options, config) {
+ initialize: function(options) {
var self = this;
this.el = $(this.el);
_.bindAll(this, 'render', 'redraw');
@@ -707,18 +788,14 @@ my.FlotGraph = Backbone.View.extend({
this.model.fields.bind('add', this.render);
this.model.currentDocuments.bind('add', this.redraw);
this.model.currentDocuments.bind('reset', this.redraw);
- var configFromHash = my.parseHashQueryString().graph;
- if (configFromHash) {
- configFromHash = JSON.parse(configFromHash);
- }
- this.chartConfig = _.extend({
+ var stateData = _.extend({
group: null,
series: [],
graphType: 'lines-and-points'
},
- configFromHash,
- config
- );
+ options.state
+ );
+ this.state = new recline.Model.ObjectState(stateData);
this.render();
},
@@ -740,13 +817,12 @@ my.FlotGraph = Backbone.View.extend({
var series = this.$series.map(function () {
return $(this).val();
});
- this.chartConfig.series = $.makeArray(series);
- this.chartConfig.group = this.el.find('.editor-group select').val();
- this.chartConfig.graphType = this.el.find('.editor-type select').val();
- // update navigation
- var qs = my.parseHashQueryString();
- qs.graph = JSON.stringify(this.chartConfig);
- my.setHashQueryString(qs);
+ var updatedState = {
+ series: $.makeArray(series),
+ group: this.el.find('.editor-group select').val(),
+ graphType: this.el.find('.editor-type select').val()
+ };
+ this.state.set(updatedState);
this.redraw();
},
@@ -762,7 +838,7 @@ my.FlotGraph = Backbone.View.extend({
return;
}
var series = this.createSeries();
- var options = this.getGraphOptions(this.chartConfig.graphType);
+ var options = this.getGraphOptions(this.state.attributes.graphType);
this.plot = $.plot(this.$graph, series, options);
this.setupTooltips();
// create this.plot and cache it
@@ -783,7 +859,7 @@ my.FlotGraph = Backbone.View.extend({
// special tickformatter to show labels rather than numbers
var tickFormatter = function (val) {
if (self.model.currentDocuments.models[val]) {
- var out = self.model.currentDocuments.models[val].get(self.chartConfig.group);
+ var out = self.model.currentDocuments.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;
@@ -866,14 +942,14 @@ my.FlotGraph = Backbone.View.extend({
var y = item.datapoint[1];
// 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.chartConfig.group);
+ x = self.model.currentDocuments.models[x].get(self.state.attributes.group);
} else {
x = x.toFixed(2);
}
y = y.toFixed(2);
var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
- group: self.chartConfig.group,
+ group: self.state.attributes.group,
x: x,
series: item.series.label,
y: y
@@ -891,25 +967,23 @@ my.FlotGraph = Backbone.View.extend({
createSeries: function () {
var self = this;
var series = [];
- if (this.chartConfig) {
- $.each(this.chartConfig.series, function (seriesIndex, field) {
- var points = [];
- $.each(self.model.currentDocuments.models, function (index, doc) {
- var x = doc.get(self.chartConfig.group);
- var y = doc.get(field);
- if (typeof x === 'string') {
- x = index;
- }
- // horizontal bar chart
- if (self.chartConfig.graphType == 'bars') {
- points.push([y, x]);
- } else {
- points.push([x, y]);
- }
- });
- series.push({data: points, label: field});
+ _.each(this.state.attributes.series, function(field) {
+ var points = [];
+ _.each(self.model.currentDocuments.models, function(doc, index) {
+ var x = doc.get(self.state.attributes.group);
+ var y = doc.get(field);
+ if (typeof x === 'string') {
+ x = index;
+ }
+ // horizontal bar chart
+ if (self.state.attributes.graphType == 'bars') {
+ points.push([y, x]);
+ } else {
+ points.push([x, y]);
+ }
});
- }
+ series.push({data: points, label: field});
+ });
return series;
},
@@ -969,12 +1043,12 @@ this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
-// ## DataGrid
+// ## (Data) Grid Dataset View
//
// Provides a tabular view on a Dataset.
//
// Initialize it with a `recline.Model.Dataset`.
-my.DataGrid = Backbone.View.extend({
+my.Grid = Backbone.View.extend({
tagName: "div",
className: "recline-grid-container",
@@ -985,12 +1059,16 @@ my.DataGrid = Backbone.View.extend({
this.model.currentDocuments.bind('add', this.render);
this.model.currentDocuments.bind('reset', this.render);
this.model.currentDocuments.bind('remove', this.render);
- this.state = {};
- this.hiddenFields = [];
+ this.tempState = {};
+ var state = _.extend({
+ hiddenFields: []
+ }, modelEtc.state
+ );
+ this.state = new recline.Model.ObjectState(state);
},
events: {
- 'click .column-header-menu': 'onColumnHeaderClick',
+ 'click .column-header-menu .data-table-menu li a': 'onColumnHeaderClick',
'click .row-header-menu': 'onRowHeaderClick',
'click .root-header-menu': 'onRootHeaderClick',
'click .data-table-menu li a': 'onMenuClick'
@@ -1012,11 +1090,11 @@ my.DataGrid = Backbone.View.extend({
// Column and row menus
onColumnHeaderClick: function(e) {
- this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field');
+ this.tempState.currentColumn = $(e.target).closest('.column-header').attr('data-field');
},
onRowHeaderClick: function(e) {
- this.state.currentRow = $(e.target).parents('tr:first').attr('data-id');
+ this.tempState.currentRow = $(e.target).parents('tr:first').attr('data-id');
},
onRootHeaderClick: function(e) {
@@ -1024,7 +1102,7 @@ my.DataGrid = Backbone.View.extend({
{{#columns}} \
<li><a data-action="showColumn" data-column="{{.}}" href="JavaScript:void(0);">Show column: {{.}}</a></li> \
{{/columns}}';
- var tmp = $.mustache(tmpl, {'columns': this.hiddenFields});
+ var tmp = $.mustache(tmpl, {'columns': this.state.get('hiddenFields')});
this.el.find('.root-header-menu .dropdown-menu').html(tmp);
},
@@ -1032,15 +1110,15 @@ my.DataGrid = Backbone.View.extend({
var self = this;
e.preventDefault();
var actions = {
- bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}); },
+ bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.tempState.currentColumn}); },
facet: function() {
- self.model.queryState.addFacet(self.state.currentColumn);
+ self.model.queryState.addFacet(self.tempState.currentColumn);
},
facet_histogram: function() {
- self.model.queryState.addHistogramFacet(self.state.currentColumn);
+ self.model.queryState.addHistogramFacet(self.tempState.currentColumn);
},
filter: function() {
- self.model.queryState.addTermFilter(self.state.currentColumn, '');
+ self.model.queryState.addTermFilter(self.tempState.currentColumn, '');
},
transform: function() { self.showTransformDialog('transform'); },
sortAsc: function() { self.setColumnSort('asc'); },
@@ -1051,7 +1129,7 @@ my.DataGrid = Backbone.View.extend({
var doc = _.find(self.model.currentDocuments.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.state.currentRow;
+ return doc.id == self.tempState.currentRow;
});
doc.destroy().then(function() {
self.model.currentDocuments.remove(doc);
@@ -1070,7 +1148,7 @@ my.DataGrid = Backbone.View.extend({
var view = new my.ColumnTransform({
model: this.model
});
- view.state = this.state;
+ view.state = this.tempState;
view.render();
$el.empty();
$el.append(view.el);
@@ -1096,17 +1174,20 @@ my.DataGrid = Backbone.View.extend({
setColumnSort: function(order) {
var sort = [{}];
- sort[0][this.state.currentColumn] = {order: order};
+ sort[0][this.tempState.currentColumn] = {order: order};
this.model.query({sort: sort});
},
hideColumn: function() {
- this.hiddenFields.push(this.state.currentColumn);
+ var hiddenFields = this.state.get('hiddenFields');
+ hiddenFields.push(this.tempState.currentColumn);
+ this.state.set({hiddenFields: hiddenFields});
this.render();
},
showColumn: function(e) {
- this.hiddenFields = _.without(this.hiddenFields, $(e.target).data('column'));
+ var hiddenFields = _.without(this.state.get('hiddenFields'), $(e.target).data('column'));
+ this.state.set({hiddenFields: hiddenFields});
this.render();
},
@@ -1162,41 +1243,41 @@ my.DataGrid = Backbone.View.extend({
render: function() {
var self = this;
this.fields = this.model.fields.filter(function(field) {
- return _.indexOf(self.hiddenFields, field.id) == -1;
+ return _.indexOf(self.state.get('hiddenFields'), field.id) == -1;
});
var htmls = $.mustache(this.template, this.toTemplateJSON());
this.el.html(htmls);
this.model.currentDocuments.forEach(function(doc) {
var tr = $('<tr />');
self.el.find('tbody').append(tr);
- var newView = new my.DataGridRow({
+ var newView = new my.GridRow({
model: doc,
el: tr,
fields: self.fields
});
newView.render();
});
- this.el.toggleClass('no-hidden', (self.hiddenFields.length === 0));
+ this.el.toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0));
return this;
}
});
-// ## DataGridRow View for rendering an individual document.
+// ## GridRow View for rendering an individual document.
//
// Since we want this to update in place it is up to creator to provider the element to attach to.
//
-// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the DataGrid.
+// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the Grid.
//
// Example:
//
// <pre>
-// var row = new DataGridRow({
+// var row = new GridRow({
// model: dataset-document,
// el: dom-element,
// fields: mydatasets.fields // a FieldList object
// });
// </pre>
-my.DataGridRow = Backbone.View.extend({
+my.GridRow = Backbone.View.extend({
initialize: function(initData) {
_.bindAll(this, 'render');
this._fields = initData.fields;
@@ -1301,21 +1382,21 @@ this.recline.View = this.recline.View || {};
// [GeoJSON](http://geojson.org) objects or two fields with latitude and
// longitude coordinates.
//
-// Initialization arguments:
-//
-// * options: initial options. They must contain a model:
-//
-// {
-// model: {recline.Model.Dataset}
-// }
-//
-// * config: (optional) map configuration hash (not yet used)
-//
+// Initialization arguments are as standard for Dataset Views. State object may
+// have the following (optional) configuration options:
//
+// <pre>
+// {
+// // geomField if specified will be used in preference to lat/lon
+// geomField: {id of field containing geometry in the dataset}
+// lonField: {id of field containing longitude in the dataset}
+// latField: {id of field containing latitude in the dataset}
+// }
+// </pre>
my.Map = Backbone.View.extend({
tagName: 'div',
- className: 'data-map-container',
+ className: 'recline-map',
template: ' \
<div class="editor"> \
@@ -1384,14 +1465,12 @@ my.Map = Backbone.View.extend({
'change .editor-field-type': 'onFieldTypeChange'
},
-
- initialize: function(options, config) {
+ initialize: function(options) {
var self = this;
-
this.el = $(this.el);
// Listen to changes in the fields
- this.model.bind('change', function() {
+ this.model.fields.bind('change', function() {
self._setupGeometryField();
});
this.model.fields.bind('add', this.render);
@@ -1408,11 +1487,21 @@ my.Map = Backbone.View.extend({
// If the div was hidden, Leaflet needs to recalculate some sizes
// to display properly
this.bind('view:show',function(){
- self.map.invalidateSize();
+ if (self.map) {
+ self.map.invalidateSize();
+ }
});
- this.mapReady = false;
+ var stateData = _.extend({
+ geomField: null,
+ lonField: null,
+ latField: null
+ },
+ options.state
+ );
+ this.state = new recline.Model.ObjectState(stateData);
+ this.mapReady = false;
this.render();
},
@@ -1429,12 +1518,12 @@ my.Map = Backbone.View.extend({
this.$map = this.el.find('.panel.map');
if (this.geomReady && this.model.fields.length){
- if (this._geomFieldName){
- this._selectOption('editor-geom-field',this._geomFieldName);
+ if (this.state.get('geomField')){
+ this._selectOption('editor-geom-field',this.state.get('geomField'));
$('#editor-field-type-geom').attr('checked','checked').change();
} else{
- this._selectOption('editor-lon-field',this._lonFieldName);
- this._selectOption('editor-lat-field',this._latFieldName);
+ this._selectOption('editor-lon-field',this.state.get('lonField'));
+ this._selectOption('editor-lat-field',this.state.get('latField'));
$('#editor-field-type-latlon').attr('checked','checked').change();
}
}
@@ -1463,9 +1552,7 @@ my.Map = Backbone.View.extend({
// * refresh: Clear existing features and add all current documents
//
redraw: function(action,doc){
-
var self = this;
-
action = action || 'refresh';
if (this.geomReady && this.mapReady){
@@ -1494,14 +1581,19 @@ my.Map = Backbone.View.extend({
onEditorSubmit: function(e){
e.preventDefault();
if ($('#editor-field-type-geom').attr('checked')){
- this._geomFieldName = $('.editor-geom-field > select > option:selected').val();
- this._latFieldName = this._lonFieldName = false;
+ this.state.set({
+ geomField: $('.editor-geom-field > select > option:selected').val(),
+ lonField: null,
+ latField: null
+ });
} else {
- this._geomFieldName = false;
- this._latFieldName = $('.editor-lat-field > select > option:selected').val();
- this._lonFieldName = $('.editor-lon-field > select > option:selected').val();
+ this.state.set({
+ geomField: null,
+ lonField: $('.editor-lon-field > select > option:selected').val(),
+ latField: $('.editor-lat-field > select > option:selected').val()
+ });
}
- this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+ this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
this.redraw();
return false;
@@ -1576,7 +1668,7 @@ my.Map = Backbone.View.extend({
if (!(docs instanceof Array)) docs = [docs];
- _.each(doc,function(doc){
+ _.each(docs,function(doc){
for (key in self.features._layers){
if (self.features._layers[key].cid == doc.cid){
self.features.removeLayer(self.features._layers[key]);
@@ -1590,16 +1682,16 @@ my.Map = Backbone.View.extend({
//
_getGeometryFromDocument: function(doc){
if (this.geomReady){
- if (this._geomFieldName){
+ if (this.state.get('geomField')){
// We assume that the contents of the field are a valid GeoJSON object
- return doc.attributes[this._geomFieldName];
- } else if (this._lonFieldName && this._latFieldName){
+ return doc.attributes[this.state.get('geomField')];
+ } else if (this.state.get('lonField') && this.state.get('latField')){
// We'll create a GeoJSON like point object from the two lat/lon fields
return {
type: 'Point',
coordinates: [
- doc.attributes[this._lonFieldName],
- doc.attributes[this._latFieldName]
+ doc.attributes[this.state.get('lonField')],
+ doc.attributes[this.state.get('latField')]
]
};
}
@@ -1613,12 +1705,12 @@ my.Map = Backbone.View.extend({
// If not found, the user can define them via the UI form.
_setupGeometryField: function(){
var geomField, latField, lonField;
-
- this._geomFieldName = this._checkField(this.geometryFieldNames);
- this._latFieldName = this._checkField(this.latitudeFieldNames);
- this._lonFieldName = this._checkField(this.longitudeFieldNames);
-
- this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+ this.state.set({
+ geomField: this._checkField(this.geometryFieldNames),
+ latField: this._checkField(this.latitudeFieldNames),
+ lonField: this._checkField(this.longitudeFieldNames)
+ });
+ this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
},
// Private: Check if a field in the current model exists in the provided
@@ -1895,6 +1987,85 @@ my.ColumnTransform = Backbone.View.extend({
})(jQuery, recline.View);
/*jshint multistr:true */
+
+// # Recline Views
+//
+// Recline Views are Backbone Views and in keeping with normal Backbone views
+// are Widgets / Components displaying something in the DOM. Like all Backbone
+// views they have a pointer to a model or a collection and is bound to an
+// element.
+//
+// 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`.
+//
+// ### Writing your own Views
+//
+// See the existing Views.
+//
+// ----
+
+// Standard JS module setup
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -1907,47 +2078,62 @@ this.recline.View = this.recline.View || {};
// var myExplorer = new model.recline.DataExplorer({
// model: {{recline.Model.Dataset instance}}
// el: {{an existing dom element}}
-// views: {{page views}}
-// config: {{config options -- see below}}
+// views: {{dataset views}}
+// state: {{state configuration -- see below}}
// });
// </pre>
//
// ### Parameters
//
-// **model**: (required) Dataset instance.
+// **model**: (required) recline.model.Dataset instance.
//
-// **el**: (required) DOM element.
+// **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 views (Grid, Graph etc) for DataExplorer to
-// show. This is an array of view hashes. If not provided
-// just initialize a DataGrid with id 'grid'. Example:
+// **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.DataGrid({
+// view: new recline.View.Grid({
// model: dataset
// })
// },
// {
// id: 'graph',
// label: 'Graph',
-// view: new recline.View.FlotGraph({
+// view: new recline.View.Graph({
// model: dataset
// })
// }
// ];
// </pre>
//
-// **config**: Config options like:
+// **state**: standard state config for this view. This state is slightly
+// special as it includes config of many of the subviews.
//
-// * readOnly: true/false (default: false) value indicating whether to
-// operate in read-only mode (hiding all editing options).
+// <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>
//
-// NB: the element already being in the DOM is important for rendering of
-// FlotGraph subview.
+// 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"> \
@@ -1956,7 +2142,7 @@ my.DataExplorer = Backbone.View.extend({
<div class="header"> \
<ul class="navigation"> \
{{#views}} \
- <li><a href="#{{id}}" class="btn">{{label}}</a> \
+ <li><a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
{{/views}} \
</ul> \
<div class="recline-results-info"> \
@@ -1979,33 +2165,53 @@ my.DataExplorer = Backbone.View.extend({
</div> \
',
events: {
- 'click .menu-right a': 'onMenuClick'
+ 'click .menu-right a': '_onMenuClick',
+ 'click .navigation a': '_onSwitchView'
},
initialize: function(options) {
var self = this;
this.el = $(this.el);
- this.config = _.extend({
- readOnly: false
- },
- options.config);
- if (this.config.readOnly) {
- this.setReadOnly();
- }
// Hash of 'page' views (i.e. those for whole page) keyed by page name
+ this._setupState(options.state);
if (options.views) {
this.pageViews = options.views;
} else {
this.pageViews = [{
id: 'grid',
label: 'Grid',
- view: new my.DataGrid({
- model: this.model
- })
+ 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')
+ }),
}];
}
- // this must be called after pageViews are created
+ // these must be called after pageViews are created
this.render();
+ this._bindStateChanges();
+ // 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.router = new Backbone.Router();
this.setupRouting();
@@ -2021,7 +2227,7 @@ my.DataExplorer = Backbone.View.extend({
var qs = my.parseHashQueryString();
qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON());
var out = my.getNewHashForQueryString(qs);
- self.router.navigate(out);
+ // self.router.navigate(out);
});
this.model.bind('query:fail', function(error) {
my.clearNotifications();
@@ -2045,11 +2251,7 @@ my.DataExplorer = Backbone.View.extend({
// note this.model and dataset returned are the same
this.model.fetch()
.done(function(dataset) {
- var queryState = my.parseHashQueryString().reclineQuery;
- if (queryState) {
- queryState = JSON.parse(queryState);
- }
- self.model.query(queryState);
+ self.model.query(self.state.get('query'));
})
.fail(function(error) {
my.notify(error.message, {category: 'error', persist: true});
@@ -2057,12 +2259,11 @@ my.DataExplorer = Backbone.View.extend({
},
setReadOnly: function() {
- this.el.addClass('read-only');
+ this.el.addClass('recline-read-only');
},
render: function() {
var tmplData = this.model.toTemplateJSON();
- tmplData.displayCount = this.config.displayCount;
tmplData.views = this.pageViews;
var template = $.mustache(this.template, tmplData);
$(this.el).html(template);
@@ -2089,20 +2290,22 @@ my.DataExplorer = Backbone.View.extend({
setupRouting: function() {
var self = this;
// Default route
- this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
- self.updateNav(self.pageViews[0].id, queryString);
- });
- $.each(this.pageViews, function(idx, view) {
- self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) {
- self.updateNav(viewId, queryString);
- });
+// this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
+// self.updateNav(self.pageViews[0].id, queryString);
+// });
+// $.each(this.pageViews, function(idx, view) {
+// self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) {
+// self.updateNav(viewId, queryString);
+// });
+// });
+ this.router.route(/.*/, 'view', function() {
});
},
- updateNav: function(pageName, queryString) {
+ 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[href=#' + pageName + ']');
+ var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]');
$el.parent().addClass('active');
$el.addClass('disabled');
// show the specific page
@@ -2117,7 +2320,7 @@ my.DataExplorer = Backbone.View.extend({
});
},
- onMenuClick: function(e) {
+ _onMenuClick: function(e) {
e.preventDefault();
var action = $(e.target).attr('data-action');
if (action === 'filters') {
@@ -2125,9 +2328,76 @@ my.DataExplorer = Backbone.View.extend({
} 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__,
+ dataset: this.model.toJSON(),
+ 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();
+ self.state.set(update);
+ });
+ }
+ });
}
});
+// ### 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: ' \
@@ -2403,6 +2673,9 @@ my.composeQueryString = function(queryParams) {
var queryString = '?';
var items = [];
$.each(queryParams, function(key, value) {
+ if (typeof(value) === 'object') {
+ value = JSON.stringify(value);
+ }
items.push(key + '=' + value);
});
queryString += items.join('&');
@@ -2484,10 +2757,20 @@ this.recline.Backend = this.recline.Backend || {};
// ## recline.Backend.Base
//
// Base class for backends providing a template and convenience functions.
- // You do not have to inherit from this class but even when not it does provide guidance on the functions you must implement.
+ // You do not have to inherit from this class but even when not it does
+ // provide guidance on the functions you must implement.
//
// Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.
my.Base = Backbone.Model.extend({
+ // ### __type__
+ //
+ // 'type' of this backend. This should be either the class path for this
+ // object as a string (e.g. recline.Backend.Memory) or for Backends within
+ // recline.Backend module it may be their class name.
+ //
+ // This value is used as an identifier for this backend when initializing
+ // backends (see recline.Model.Dataset.initialize).
+ __type__: 'base',
// ### sync
//
@@ -2607,6 +2890,7 @@ this.recline.Backend = this.recline.Backend || {};
//
// Note that this is a **read-only** backend.
my.DataProxy = my.Base.extend({
+ __type__: 'dataproxy',
defaults: {
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
},
@@ -2661,8 +2945,6 @@ this.recline.Backend = this.recline.Backend || {};
return dfd.promise();
}
});
- recline.Model.backends['dataproxy'] = new my.DataProxy();
-
}(jQuery, this.recline.Backend));
this.recline = this.recline || {};
@@ -2687,6 +2969,7 @@ this.recline.Backend = this.recline.Backend || {};
//
// <pre>http://localhost:9200/twitter/tweet</pre>
my.ElasticSearch = my.Base.extend({
+ __type__: 'elasticsearch',
_getESUrl: function(dataset) {
var out = dataset.get('elasticsearch_url');
if (out) return out;
@@ -2782,7 +3065,6 @@ this.recline.Backend = this.recline.Backend || {};
return dfd.promise();
}
});
- recline.Model.backends['elasticsearch'] = new my.ElasticSearch();
}(jQuery, this.recline.Backend));
@@ -2805,6 +3087,7 @@ this.recline.Backend = this.recline.Backend || {};
// );
// </pre>
my.GDoc = my.Base.extend({
+ __type__: 'gdoc',
getUrl: function(dataset) {
var url = dataset.get('url');
if (url.indexOf('feeds/list') != -1) {
@@ -2922,7 +3205,6 @@ this.recline.Backend = this.recline.Backend || {};
return results;
}
});
- recline.Model.backends['gdocs'] = new my.GDoc();
}(jQuery, this.recline.Backend));
@@ -2930,7 +3212,9 @@ this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
(function($, my) {
- my.loadFromCSVFile = function(file, callback) {
+ my.loadFromCSVFile = function(file, callback, options) {
+ var encoding = options.encoding || 'UTF-8';
+
var metadata = {
id: file.name,
file: file
@@ -2938,17 +3222,17 @@ this.recline.Backend = this.recline.Backend || {};
var reader = new FileReader();
// TODO
reader.onload = function(e) {
- var dataset = my.csvToDataset(e.target.result);
+ var dataset = my.csvToDataset(e.target.result, options);
callback(dataset);
};
reader.onerror = function (e) {
alert('Failed to load file. Code: ' + e.target.error.code);
};
- reader.readAsText(file);
+ reader.readAsText(file, encoding);
};
- my.csvToDataset = function(csvString) {
- var out = my.parseCSV(csvString);
+ my.csvToDataset = function(csvString, options) {
+ var out = my.parseCSV(csvString, options);
fields = _.map(out[0], function(cell) {
return { id: cell, label: cell };
});
@@ -2963,128 +3247,133 @@ this.recline.Backend = this.recline.Backend || {};
return dataset;
};
- // Converts a Comma Separated Values string into an array of arrays.
- // Each line in the CSV becomes an array.
+ // Converts a Comma Separated Values string into an array of arrays.
+ // Each line in the CSV becomes an array.
//
- // Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats.
- //
- // @return The CSV parsed as an array
- // @type Array
- //
- // @param {String} s The string to convert
- // @param {Boolean} [trm=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported
+ // Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats.
//
+ // @return The CSV parsed as an array
+ // @type Array
+ //
+ // @param {String} s The string to convert
+ // @param {Object} options Options for loading CSV including
+ // @param {Boolean} [trim=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported
+ // @param {String} [separator=','] Separator for CSV file
// Heavily based on uselesscode's JS CSV parser (MIT Licensed):
// thttp://www.uselesscode.org/javascript/csv/
- my.parseCSV= function(s, trm) {
- // Get rid of any trailing \n
- s = chomp(s);
-
- var cur = '', // The character we are currently processing.
- inQuote = false,
- fieldQuoted = false,
- field = '', // Buffer for building up the current field
- row = [],
- out = [],
- i,
- processField;
-
- processField = function (field) {
- if (fieldQuoted !== true) {
- // If field is empty set to null
- if (field === '') {
- field = null;
- // If the field was not quoted and we are trimming fields, trim it
- } else if (trm === true) {
- field = trim(field);
- }
-
- // Convert unquoted numbers to their appropriate types
- if (rxIsInt.test(field)) {
- field = parseInt(field, 10);
- } else if (rxIsFloat.test(field)) {
- field = parseFloat(field, 10);
- }
- }
- return field;
- };
+ my.parseCSV= function(s, options) {
+ // Get rid of any trailing \n
+ s = chomp(s);
+
+ var options = options || {};
+ var trm = options.trim;
+ var separator = options.separator || ',';
+
+ var cur = '', // The character we are currently processing.
+ inQuote = false,
+ fieldQuoted = false,
+ field = '', // Buffer for building up the current field
+ row = [],
+ out = [],
+ i,
+ processField;
+
+ processField = function (field) {
+ if (fieldQuoted !== true) {
+ // If field is empty set to null
+ if (field === '') {
+ field = null;
+ // If the field was not quoted and we are trimming fields, trim it
+ } else if (trm === true) {
+ field = trim(field);
+ }
+
+ // Convert unquoted numbers to their appropriate types
+ if (rxIsInt.test(field)) {
+ field = parseInt(field, 10);
+ } else if (rxIsFloat.test(field)) {
+ field = parseFloat(field, 10);
+ }
+ }
+ return field;
+ };
+
+ for (i = 0; i < s.length; i += 1) {
+ cur = s.charAt(i);
+
+ // If we are at a EOF or EOR
+ if (inQuote === false && (cur === separator || cur === "\n")) {
+ field = processField(field);
+ // Add the current field to the current row
+ row.push(field);
+ // If this is EOR append row to output and flush row
+ if (cur === "\n") {
+ out.push(row);
+ row = [];
+ }
+ // Flush the field buffer
+ field = '';
+ fieldQuoted = false;
+ } else {
+ // If it's not a ", add it to the field buffer
+ if (cur !== '"') {
+ field += cur;
+ } else {
+ if (!inQuote) {
+ // We are not in a quote, start a quote
+ inQuote = true;
+ fieldQuoted = true;
+ } else {
+ // Next char is ", this is an escaped "
+ if (s.charAt(i + 1) === '"') {
+ field += '"';
+ // Skip the next char
+ i += 1;
+ } else {
+ // It's not escaping, so end quote
+ inQuote = false;
+ }
+ }
+ }
+ }
+ }
- for (i = 0; i < s.length; i += 1) {
- cur = s.charAt(i);
-
- // If we are at a EOF or EOR
- if (inQuote === false && (cur === ',' || cur === "\n")) {
- field = processField(field);
- // Add the current field to the current row
- row.push(field);
- // If this is EOR append row to output and flush row
- if (cur === "\n") {
- out.push(row);
- row = [];
- }
- // Flush the field buffer
- field = '';
- fieldQuoted = false;
- } else {
- // If it's not a ", add it to the field buffer
- if (cur !== '"') {
- field += cur;
- } else {
- if (!inQuote) {
- // We are not in a quote, start a quote
- inQuote = true;
- fieldQuoted = true;
- } else {
- // Next char is ", this is an escaped "
- if (s.charAt(i + 1) === '"') {
- field += '"';
- // Skip the next char
- i += 1;
- } else {
- // It's not escaping, so end quote
- inQuote = false;
- }
- }
- }
- }
- }
-
- // Add the last field
- field = processField(field);
- row.push(field);
- out.push(row);
-
- return out;
- };
-
- var rxIsInt = /^\d+$/,
- rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,
- // If a string has leading or trailing space,
- // contains a comma double quote or a newline
- // it needs to be quoted in CSV output
- rxNeedsQuoting = /^\s|\s$|,|"|\n/,
- trim = (function () {
- // Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists
- if (String.prototype.trim) {
- return function (s) {
- return s.trim();
- };
- } else {
- return function (s) {
- return s.replace(/^\s*/, '').replace(/\s*$/, '');
- };
- }
- }());
-
- function chomp(s) {
- if (s.charAt(s.length - 1) !== "\n") {
- // Does not end with \n, just return string
- return s;
- } else {
- // Remove the \n
- return s.substring(0, s.length - 1);
- }
- }
+ // Add the last field
+ field = processField(field);
+ row.push(field);
+ out.push(row);
+
+ return out;
+ };
+
+ var rxIsInt = /^\d+$/,
+ rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,
+ // If a string has leading or trailing space,
+ // contains a comma double quote or a newline
+ // it needs to be quoted in CSV output
+ rxNeedsQuoting = /^\s|\s$|,|"|\n/,
+ trim = (function () {
+ // Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists
+ if (String.prototype.trim) {
+ return function (s) {
+ return s.trim();
+ };
+ } else {
+ return function (s) {
+ return s.replace(/^\s*/, '').replace(/\s*$/, '');
+ };
+ }
+ }());
+
+ function chomp(s) {
+ if (s.charAt(s.length - 1) !== "\n") {
+ // Does not end with \n, just return string
+ return s;
+ } else {
+ // Remove the \n
+ return s.substring(0, s.length - 1);
+ }
+ }
}(jQuery, this.recline.Backend));
@@ -3110,7 +3399,7 @@ this.recline.Backend = this.recline.Backend || {};
if (!metadata.id) {
metadata.id = String(Math.floor(Math.random() * 100000000) + 1);
}
- var backend = recline.Model.backends['memory'];
+ var backend = new recline.Backend.Memory();
var datasetInfo = {
documents: data,
metadata: metadata
@@ -3125,7 +3414,7 @@ this.recline.Backend = this.recline.Backend || {};
}
}
backend.addDataset(datasetInfo);
- var dataset = new recline.Model.Dataset({id: metadata.id}, 'memory');
+ var dataset = new recline.Model.Dataset({id: metadata.id}, backend);
dataset.fetch();
return dataset;
};
@@ -3160,6 +3449,7 @@ this.recline.Backend = this.recline.Backend || {};
// etc ...
// </pre>
my.Memory = my.Base.extend({
+ __type__: 'memory',
initialize: function() {
this.datasets = {};
},
@@ -3207,13 +3497,9 @@ this.recline.Backend = this.recline.Backend || {};
var out = {};
var numRows = queryObj.size;
var start = queryObj.from;
- results = this.datasets[model.id].documents;
- _.each(queryObj.filters, function(filter) {
- results = _.filter(results, function(doc) {
- var fieldId = _.keys(filter.term)[0];
- return (doc[fieldId] == filter.term[fieldId]);
- });
- });
+ var results = this.datasets[model.id].documents;
+ results = this._applyFilters(results, queryObj);
+ results = this._applyFreeTextQuery(model, results, queryObj);
// not complete sorting!
_.each(queryObj.sort, function(sortObj) {
var fieldName = _.keys(sortObj)[0];
@@ -3231,6 +3517,42 @@ this.recline.Backend = this.recline.Backend || {};
return dfd.promise();
},
+ // in place filtering
+ _applyFilters: function(results, queryObj) {
+ _.each(queryObj.filters, function(filter) {
+ results = _.filter(results, function(doc) {
+ var fieldId = _.keys(filter.term)[0];
+ return (doc[fieldId] == filter.term[fieldId]);
+ });
+ });
+ return results;
+ },
+
+ // we OR across fields but AND across terms in query string
+ _applyFreeTextQuery: function(dataset, results, queryObj) {
+ if (queryObj.q) {
+ var terms = queryObj.q.split(' ');
+ results = _.filter(results, function(rawdoc) {
+ var matches = true;
+ _.each(terms, function(term) {
+ var foundmatch = false;
+ dataset.fields.each(function(field) {
+ var value = rawdoc[field.id].toString();
+ // TODO regexes?
+ foundmatch = foundmatch || (value === term);
+ // TODO: early out (once we are true should break to spare unnecessary testing)
+ // if (foundmatch) return true;
+ });
+ matches = matches && foundmatch;
+ // TODO: early out (once false should break to spare unnecessary testing)
+ // if (!matches) return false;
+ });
+ return matches;
+ });
+ }
+ return results;
+ },
+
_computeFacets: function(documents, queryObj) {
var facetResults = {};
if (!queryObj.facets) {
@@ -3267,6 +3589,5 @@ this.recline.Backend = this.recline.Backend || {};
return facetResults;
}
});
- recline.Model.backends['memory'] = new my.Memory();
}(jQuery, this.recline.Backend));
================================================================
Commit: bc2fd64e31862a8458d224b295e82fc90f1905a9
https://github.com/okfn/ckan/commit/bc2fd64e31862a8458d224b295e82fc90f1905a9
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-20 (Fri, 20 Apr 2012)
Changed paths:
M ckan/config/routing.py
M ckan/controllers/package.py
M ckan/public/scripts/application.js
A ckan/templates/package/resource_embeded_dataviewer.html
M ckan/templates/package/resource_read.html
Log Message:
-----------
[2285] First, very rough stab at making the data viewer embeddable.
diff --git a/ckan/config/routing.py b/ckan/config/routing.py
index f3ae5b8..58d74dd 100644
--- a/ckan/config/routing.py
+++ b/ckan/config/routing.py
@@ -183,6 +183,7 @@ def make_map():
m.connect('/dataset/{id}.{format}', action='read')
m.connect('/dataset/{id}', action='read')
m.connect('/dataset/{id}/resource/{resource_id}', action='resource_read')
+ m.connect('/dataset/{id}/resource/{resource_id}/embed', action='resource_embeded_dataviewer')
# group
map.redirect('/groups', '/group')
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index 90ebca4..bf34d30 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -734,3 +734,51 @@ def resource_read(self, id, resource_id):
qualified=True)
return render('package/resource_read.html')
+ def resource_embeded_dataviewer(self, id, resource_id):
+ """
+ Embeded page for a read-only resource dataview.
+ """
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author}
+
+ try:
+ resource = get_action('resource_show')(context, {'id': resource_id})
+ package = get_action('package_show')(context, {'id': id})
+
+ # These are just required whilst still basing this off the resource_read
+ c.resource = get_action('resource_show')(context, {'id': resource_id})
+ c.package = get_action('package_show')(context, {'id': id})
+ c.pkg = context['package']
+ c.resource_json = json.dumps(c.resource)
+ c.pkg_dict = c.package
+
+ # double check that the resource belongs to the specified package
+ if not resource['id'] in [ r['id'] for r in package['resources'] ]:
+ raise NotFound
+
+ c.resource_json = json.dumps(resource)
+
+ except NotFound:
+ abort(404, _('Resource not found'))
+ except NotAuthorized:
+ abort(401, _('Unauthorized to read resource %s') % id)
+
+ # Construct the recline state
+ state_version = int(request.params.get('state_version', '1'))
+ raw_state = request.params.get('state', '')
+ recline_state = self._parse_recline_state(state_version, raw_state)
+ if recline_state is None:
+ abort(400, ('"state" parameter must be a valid recline state (version %d)' % state_version))
+
+ c.recline_state = json.dumps(recline_state)
+
+ return render('package/resource_embeded_dataviewer.html')
+
+ def _parse_recline_state(self, state_version, raw_state):
+ if state_version != 1: # Only support one version at the moment
+ return None
+
+ try:
+ return json.loads(raw_state)
+ except ValueError:
+ return None
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 995e703..f3772a3 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -44,6 +44,12 @@ CKAN.Utils = CKAN.Utils || {};
if (isResourceView) {
CKAN.DataPreview.loadPreviewDialog(preload_resource);
}
+
+ var isEmbededDataviewer = $('body.package.resource_embeded_dataviewer').length > 0;
+ if (isEmbededDataviewer) {
+ CKAN.DataPreview.loadPreviewDialogWithState(preload_resource, reclineState);
+ }
+
var isDatasetNew = $('body.package.new').length > 0;
if (isDatasetNew) {
// Set up magic URL slug editor
@@ -1250,6 +1256,29 @@ CKAN.DataPreview = function ($, my) {
my.dialogId = 'ckanext-datapreview';
my.$dialog = $('#' + my.dialogId);
+ // **Public: Loads a data preview, taking into account an initial state**
+ //
+ my.loadPreviewDialogWithState = function(resourceData, reclineState) {
+ my.$dialog.html('<h4>Loading ... <img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" class="loading-spinner" /></h4>');
+
+ var dataset = recline.Model.Dataset.restore(reclineState);
+ var dataExplorer = new recline.View.DataExplorer({
+ el: my.$dialog,
+ model: dataset,
+ state: reclineState
+ });
+
+ Backbone.history.start();
+ };
+
+ my.makePermalink = function(explorerState) {
+ var qs = recline.View.composeQueryString({
+ state: explorerState.toJSON(),
+ state_version: 1
+ });
+ return window.location.origin + window.location.pathname + '/embed' + qs;
+ };
+
// **Public: Loads a data preview**
//
// Fetches the preview data object from the link provided and loads the
@@ -1267,14 +1296,14 @@ CKAN.DataPreview = function ($, my) {
{
id: 'grid',
label: 'Grid',
- view: new recline.View.DataGrid({
+ view: new recline.View.Grid({
model: dataset
})
},
{
id: 'graph',
label: 'Graph',
- view: new recline.View.FlotGraph({
+ view: new recline.View.Graph({
model: dataset
})
},
@@ -1294,6 +1323,13 @@ CKAN.DataPreview = function ($, my) {
readOnly: true
}
});
+
+ var permalink = $('.permalink');
+ dataExplorer.state.bind('change', function() {
+ permalink.attr('href', my.makePermalink(dataExplorer.state));
+ });
+ permalink.attr('href', my.makePermalink(dataExplorer.state));
+
// will have to refactor if this can get called multiple times
Backbone.history.start();
}
diff --git a/ckan/templates/package/resource_embeded_dataviewer.html b/ckan/templates/package/resource_embeded_dataviewer.html
new file mode 100644
index 0000000..fdc5a8d
--- /dev/null
+++ b/ckan/templates/package/resource_embeded_dataviewer.html
@@ -0,0 +1,82 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:i18n="http://genshi.edgewall.org/i18n"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ py:strip="">
+
+ <py:def function="optional_head">
+ <!-- data preview -->
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.css')}" />
+ <!--[if lte IE 8]>
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.ie.css')}" />
+ <![endif]-->
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/data-explorer.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph-flot.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/map.css')}" />
+ <style type="text/css">
+ .recline-query-editor form, .recline-query-editor .text-query {
+ height: 28px;
+ }
+
+ .recline-query-editor .pagination ul {
+ margin: 0;
+ padding: 0;
+ }
+
+ /* needed for Chrome but not FF */
+ .header .recline-query-editor .add-on {
+ margin-left: -27px;
+ }
+
+ /* needed for FF but not chrome */
+ .header .recline-query-editor .input-prepend {
+ vertical-align: top;
+ }
+ </style>
+ <!-- /data preview -->
+ <style type="text/css">
+ .resource-actions {
+ margin-right: 0;
+ }
+ .resource-actions .btn {
+ position: relative;
+ bottom: 3px;
+ padding: 8px 10px;
+ }
+ .resource-actions .download {
+ display: inline;
+ }
+ .resource-actions .download img {
+ margin: 0px 4px -4px 0;
+ }
+ </style>
+ <script type="text/javascript">
+ var preload_resource = ${h.literal(c.resource_json)};
+ var reclineState = ${h.literal(c.recline_state)};
+ </script>
+ </py:def>
+
+ <py:def function="page_title">
+ ${h.dataset_display_name(c.package)} /
+ ${h.resource_display_name(c.resource)} - Dataset - Resource
+ </py:def>
+
+ <div py:match="content">
+ <div class="resource-preview">
+ <h3>Preview</h3>
+ <a class="permalink" href="">Permalink</a>
+ <div id="ckanext-datapreview"></div>
+ </div>
+ </div>
+
+ <py:def function="optional_footer">
+ <!-- data preview -->
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/jquery.mustache/jquery.mustache.js')}"></script>
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js')}"></script>
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js')}"></script>
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.js')}"></script>
+ <script src="${h.url_for_static('/scripts/vendor/recline/recline.js')}"></script>
+ </py:def>
+
+ <xi:include href="../layout_base.html" />
+</html>
diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html
index b6c30e6..e5809c7 100644
--- a/ckan/templates/package/resource_read.html
+++ b/ckan/templates/package/resource_read.html
@@ -152,6 +152,7 @@
<div class="resource-preview">
<h3>Preview</h3>
+ <a class="permalink" href="">Permalink</a>
<div id="ckanext-datapreview"></div>
</div>
================================================================
Commit: b07d1df011d1cfc311950a50aa575c918ba2d27b
https://github.com/okfn/ckan/commit/b07d1df011d1cfc311950a50aa575c918ba2d27b
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-20 (Fri, 20 Apr 2012)
Changed paths:
M ckan/config/routing.py
M ckan/controllers/package.py
M ckan/public/scripts/application.js
A ckan/templates/package/resource_embedded_dataviewer.html
R ckan/templates/package/resource_embeded_dataviewer.html
Log Message:
-----------
[2285] Correct my bad spelling.
embeded ~> embedded
diff --git a/ckan/config/routing.py b/ckan/config/routing.py
index 58d74dd..c981a5a 100644
--- a/ckan/config/routing.py
+++ b/ckan/config/routing.py
@@ -183,7 +183,7 @@ def make_map():
m.connect('/dataset/{id}.{format}', action='read')
m.connect('/dataset/{id}', action='read')
m.connect('/dataset/{id}/resource/{resource_id}', action='resource_read')
- m.connect('/dataset/{id}/resource/{resource_id}/embed', action='resource_embeded_dataviewer')
+ m.connect('/dataset/{id}/resource/{resource_id}/embed', action='resource_embedded_dataviewer')
# group
map.redirect('/groups', '/group')
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index bf34d30..df1896f 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -734,7 +734,7 @@ def resource_read(self, id, resource_id):
qualified=True)
return render('package/resource_read.html')
- def resource_embeded_dataviewer(self, id, resource_id):
+ def resource_embedded_dataviewer(self, id, resource_id):
"""
Embeded page for a read-only resource dataview.
"""
@@ -772,7 +772,7 @@ def resource_embeded_dataviewer(self, id, resource_id):
c.recline_state = json.dumps(recline_state)
- return render('package/resource_embeded_dataviewer.html')
+ return render('package/resource_embedded_dataviewer.html')
def _parse_recline_state(self, state_version, raw_state):
if state_version != 1: # Only support one version at the moment
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index f3772a3..717f70a 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -45,7 +45,7 @@ CKAN.Utils = CKAN.Utils || {};
CKAN.DataPreview.loadPreviewDialog(preload_resource);
}
- var isEmbededDataviewer = $('body.package.resource_embeded_dataviewer').length > 0;
+ var isEmbededDataviewer = $('body.package.resource_embedded_dataviewer').length > 0;
if (isEmbededDataviewer) {
CKAN.DataPreview.loadPreviewDialogWithState(preload_resource, reclineState);
}
diff --git a/ckan/templates/package/resource_embedded_dataviewer.html b/ckan/templates/package/resource_embedded_dataviewer.html
new file mode 100644
index 0000000..fdc5a8d
--- /dev/null
+++ b/ckan/templates/package/resource_embedded_dataviewer.html
@@ -0,0 +1,82 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:i18n="http://genshi.edgewall.org/i18n"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ py:strip="">
+
+ <py:def function="optional_head">
+ <!-- data preview -->
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.css')}" />
+ <!--[if lte IE 8]>
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.ie.css')}" />
+ <![endif]-->
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/data-explorer.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph-flot.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/map.css')}" />
+ <style type="text/css">
+ .recline-query-editor form, .recline-query-editor .text-query {
+ height: 28px;
+ }
+
+ .recline-query-editor .pagination ul {
+ margin: 0;
+ padding: 0;
+ }
+
+ /* needed for Chrome but not FF */
+ .header .recline-query-editor .add-on {
+ margin-left: -27px;
+ }
+
+ /* needed for FF but not chrome */
+ .header .recline-query-editor .input-prepend {
+ vertical-align: top;
+ }
+ </style>
+ <!-- /data preview -->
+ <style type="text/css">
+ .resource-actions {
+ margin-right: 0;
+ }
+ .resource-actions .btn {
+ position: relative;
+ bottom: 3px;
+ padding: 8px 10px;
+ }
+ .resource-actions .download {
+ display: inline;
+ }
+ .resource-actions .download img {
+ margin: 0px 4px -4px 0;
+ }
+ </style>
+ <script type="text/javascript">
+ var preload_resource = ${h.literal(c.resource_json)};
+ var reclineState = ${h.literal(c.recline_state)};
+ </script>
+ </py:def>
+
+ <py:def function="page_title">
+ ${h.dataset_display_name(c.package)} /
+ ${h.resource_display_name(c.resource)} - Dataset - Resource
+ </py:def>
+
+ <div py:match="content">
+ <div class="resource-preview">
+ <h3>Preview</h3>
+ <a class="permalink" href="">Permalink</a>
+ <div id="ckanext-datapreview"></div>
+ </div>
+ </div>
+
+ <py:def function="optional_footer">
+ <!-- data preview -->
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/jquery.mustache/jquery.mustache.js')}"></script>
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js')}"></script>
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js')}"></script>
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.js')}"></script>
+ <script src="${h.url_for_static('/scripts/vendor/recline/recline.js')}"></script>
+ </py:def>
+
+ <xi:include href="../layout_base.html" />
+</html>
diff --git a/ckan/templates/package/resource_embeded_dataviewer.html b/ckan/templates/package/resource_embeded_dataviewer.html
deleted file mode 100644
index fdc5a8d..0000000
--- a/ckan/templates/package/resource_embeded_dataviewer.html
+++ /dev/null
@@ -1,82 +0,0 @@
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:i18n="http://genshi.edgewall.org/i18n"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude"
- py:strip="">
-
- <py:def function="optional_head">
- <!-- data preview -->
- <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.css')}" />
- <!--[if lte IE 8]>
- <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.ie.css')}" />
- <![endif]-->
- <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/data-explorer.css')}" />
- <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph-flot.css')}" />
- <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/map.css')}" />
- <style type="text/css">
- .recline-query-editor form, .recline-query-editor .text-query {
- height: 28px;
- }
-
- .recline-query-editor .pagination ul {
- margin: 0;
- padding: 0;
- }
-
- /* needed for Chrome but not FF */
- .header .recline-query-editor .add-on {
- margin-left: -27px;
- }
-
- /* needed for FF but not chrome */
- .header .recline-query-editor .input-prepend {
- vertical-align: top;
- }
- </style>
- <!-- /data preview -->
- <style type="text/css">
- .resource-actions {
- margin-right: 0;
- }
- .resource-actions .btn {
- position: relative;
- bottom: 3px;
- padding: 8px 10px;
- }
- .resource-actions .download {
- display: inline;
- }
- .resource-actions .download img {
- margin: 0px 4px -4px 0;
- }
- </style>
- <script type="text/javascript">
- var preload_resource = ${h.literal(c.resource_json)};
- var reclineState = ${h.literal(c.recline_state)};
- </script>
- </py:def>
-
- <py:def function="page_title">
- ${h.dataset_display_name(c.package)} /
- ${h.resource_display_name(c.resource)} - Dataset - Resource
- </py:def>
-
- <div py:match="content">
- <div class="resource-preview">
- <h3>Preview</h3>
- <a class="permalink" href="">Permalink</a>
- <div id="ckanext-datapreview"></div>
- </div>
- </div>
-
- <py:def function="optional_footer">
- <!-- data preview -->
- <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/jquery.mustache/jquery.mustache.js')}"></script>
- <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js')}"></script>
- <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js')}"></script>
- <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.js')}"></script>
- <script src="${h.url_for_static('/scripts/vendor/recline/recline.js')}"></script>
- </py:def>
-
- <xi:include href="../layout_base.html" />
-</html>
================================================================
Commit: b41095e779318de7f697ab17fce8994e23ebc086
https://github.com/okfn/ckan/commit/b41095e779318de7f697ab17fce8994e23ebc086
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-20 (Fri, 20 Apr 2012)
Changed paths:
M ckan/controllers/package.py
M ckan/public/scripts/application.js
Log Message:
-----------
[2285] Ensure only the currentView is shown in the embeddable page.
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index df1896f..4d2edcf 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -779,6 +779,18 @@ def _parse_recline_state(self, state_version, raw_state):
return None
try:
- return json.loads(raw_state)
+ state = json.loads(raw_state)
except ValueError:
return None
+
+ # Ensure the state is readOnly
+ state['readOnly'] = True
+
+ # Ensure only the currentView is available
+ if not state.get('currentView', None):
+ state['currentView'] = 'grid' # default to grid view if none specified
+ for k in state.keys():
+ if k.startswith('view-') and not k.endswith(state['currentView']):
+ state.pop(k)
+
+ return state
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 717f70a..637dc7b 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -1262,10 +1262,44 @@ CKAN.DataPreview = function ($, my) {
my.$dialog.html('<h4>Loading ... <img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" class="loading-spinner" /></h4>');
var dataset = recline.Model.Dataset.restore(reclineState);
+
+ // Only load a single view
+ // TODO: tidy this up.
+ var views = null;
+ if (reclineState.currentView === 'grid') {
+ views = [ {
+ id: 'grid',
+ label: 'Grid',
+ view: new recline.View.Grid({
+ model: dataset,
+ state: reclineState['view-grid']
+ })
+ }];
+ } else if (reclineState.currentView === 'graph') {
+ views = [ {
+ id: 'graph',
+ label: 'Graph',
+ view: new recline.View.Graph({
+ model: dataset,
+ state: reclineState['view-graph']
+ })
+ }];
+ } else if (reclineState.currentView === 'map') {
+ views = [ {
+ id: 'map',
+ label: 'Map',
+ view: new recline.View.Map({
+ model: dataset,
+ state: reclineState['view-map']
+ })
+ }];
+ }
+
var dataExplorer = new recline.View.DataExplorer({
el: my.$dialog,
model: dataset,
- state: reclineState
+ state: reclineState,
+ views: views
});
Backbone.history.start();
================================================================
Commit: 5f5ecc87e1d27ac76eabe674960ce5007f6ae9b9
https://github.com/okfn/ckan/commit/5f5ecc87e1d27ac76eabe674960ce5007f6ae9b9
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-23 (Mon, 23 Apr 2012)
Changed paths:
M ckan/templates/package/resource_embedded_dataviewer.html
Log Message:
-----------
[2285] Hide query controls using css; and move the viewer to the top-left
... of the embeddable page.
diff --git a/ckan/templates/package/resource_embedded_dataviewer.html b/ckan/templates/package/resource_embedded_dataviewer.html
index fdc5a8d..6f9b4bf 100644
--- a/ckan/templates/package/resource_embedded_dataviewer.html
+++ b/ckan/templates/package/resource_embedded_dataviewer.html
@@ -14,41 +14,33 @@
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph-flot.css')}" />
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/map.css')}" />
<style type="text/css">
- .recline-query-editor form, .recline-query-editor .text-query {
- height: 28px;
- }
- .recline-query-editor .pagination ul {
- margin: 0;
- padding: 0;
+ /* Hide the query controls */
+ .header {
+ display: none;
}
- /* needed for Chrome but not FF */
- .header .recline-query-editor .add-on {
- margin-left: -27px;
+ /* Hide CKAN footer */
+ .footer.outer {
+ display: none;
}
- /* needed for FF but not chrome */
- .header .recline-query-editor .input-prepend {
- vertical-align: top;
- }
- </style>
- <!-- /data preview -->
- <style type="text/css">
- .resource-actions {
- margin-right: 0;
+ /* Don't center the main container. And provide a little space to the
+ left and above the viewer. This is for the graph-view, which, if a
+ small amount of room is not given, the y-axis' labels are uncomfortably
+ close to the edge of the viewport.
+ */
+ #main.container {
+ width: auto;
+ margin-left: 2px;
+ margin-top: 2px;
}
- .resource-actions .btn {
- position: relative;
- bottom: 3px;
- padding: 8px 10px;
- }
- .resource-actions .download {
- display: inline;
- }
- .resource-actions .download img {
- margin: 0px 4px -4px 0;
+
+ /* Remove the border from the right-hand-side */
+ #content {
+ border: 0px;
}
+
</style>
<script type="text/javascript">
var preload_resource = ${h.literal(c.resource_json)};
@@ -63,8 +55,6 @@
<div py:match="content">
<div class="resource-preview">
- <h3>Preview</h3>
- <a class="permalink" href="">Permalink</a>
<div id="ckanext-datapreview"></div>
</div>
</div>
================================================================
Commit: 921c9a3c1f82adb3cb632bd00c537708a8233934
https://github.com/okfn/ckan/commit/921c9a3c1f82adb3cb632bd00c537708a8233934
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-23 (Mon, 23 Apr 2012)
Changed paths:
M ckan/public/scripts/vendor/recline/css/data-explorer.css
R ckan/public/scripts/vendor/recline/css/graph-flot.css
A ckan/public/scripts/vendor/recline/css/graph.css
A ckan/public/scripts/vendor/recline/css/grid.css
M ckan/templates/package/resource_embedded_dataviewer.html
M ckan/templates/package/resource_read.html
Log Message:
-----------
[2285] Updated the recline stylesheets.
- graph-flot.css renamed to graph.css
- grid.css is new (style split out from data-explorerer.css)
This pulls in the recline read-only styles for the grid view.
diff --git a/ckan/public/scripts/vendor/recline/css/data-explorer.css b/ckan/public/scripts/vendor/recline/css/data-explorer.css
index 6cc5b0a..766a91e 100644
--- a/ckan/public/scripts/vendor/recline/css/data-explorer.css
+++ b/ckan/public/scripts/vendor/recline/css/data-explorer.css
@@ -78,320 +78,3 @@
display: inline-block;
}
-
-/**********************************************************
- * Data Table
- *********************************************************/
-
-.recline-grid .btn-group .dropdown-toggle {
- padding: 1px 3px;
- line-height: auto;
-}
-
-.recline-grid-container {
- overflow: auto;
- height: 550px;
-}
-
-.recline-grid {
- border: 1px solid #ccc;
- width: 100%;
-}
-
-.recline-grid td, .recline-grid th {
- border-left: 1px solid #ccc;
- padding: 3px 4px;
- text-align: left;
-}
-
-.recline-grid tr td:first-child, .recline-grid tr th:first-child {
- width: 20px;
-}
-
-/* direct borrowing from twitter buttons */
-.recline-grid th,
-.transform-column-view .expression-preview-table-wrapper th
-{
- background-color: #e6e6e6;
- background-repeat: no-repeat;
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));
- background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
- background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);
- background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
- background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
- background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);
- text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
- color: #333;
- border: 1px solid #ccc;
- border-bottom-color: #bbb;
- -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
- -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
- -webkit-transition: 0.1s linear all;
- -moz-transition: 0.1s linear all;
- -ms-transition: 0.1s linear all;
- -o-transition: 0.1s linear all;
- transition: 0.1s linear all;
-}
-
-
-/**********************************************************
- * Data Table Menus
- *********************************************************/
-
-.column-header-menu, a.root-header-menu {
- float: right;
-}
-
-.read-only a.row-header-menu {
- display: none;
-}
-
-div.data-table-cell-content {
- line-height: 1.2;
- color: #222;
- position: relative;
-}
-
-div.data-table-cell-content-numeric {
- text-align: right;
-}
-
-a.data-table-cell-edit {
- position: absolute;
- top: 0;
- right: 0;
- display: block;
- width: 25px;
- height: 16px;
- text-decoration: none;
- background-image: url(images/edit-map.png);
- background-repeat: no-repeat;
- visibility: hidden;
-}
-
-a.data-table-cell-edit:hover {
- background-position: -25px 0px;
-}
-
-.recline-grid td:hover .data-table-cell-edit {
- visibility: visible;
-}
-
-div.data-table-cell-content-numeric > a.data-table-cell-edit {
- left: 0px;
- right: auto;
-}
-
-.data-table-value-nonstring {
- color: #282;
-}
-
-.data-table-error {
- color: red;
-}
-
-.data-table-cell-editor-editor {
- overflow: hidden;
- display: block;
- width: 98%;
- height: 3em;
- font-family: monospace;
- margin: 3px 0;
-}
-
-.data-table-cell-copypaste-editor {
- overflow: hidden;
- display: block;
- width: 98%;
- height: 10em;
- font-family: monospace;
- margin: 3px 0;
-}
-
-.data-table-cell-editor-action {
- float: left;
- vertical-align: bottom;
- text-align: center;
-}
-
-.data-table-cell-editor-key {
- font-size: 0.8em;
- color: #999;
-}
-
-
-/**********************************************************
- * Dialogs
- *********************************************************/
-
-.dialog-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: #666;
- opacity: 0.5;
-}
-
-.dialog {
- position: fixed;
- left: 0;
- width: 100%;
- text-align: center;
-}
-
-.dialog-frame {
- margin: 0 auto;
- text-align: left;
- background: white;
- border: 1px solid #3a5774;
-}
-
-.dialog-border {
- border: 4px solid #c1d9ff;
-}
-
-.dialog-header {
- background: #e0edfe;
- padding: 10px;
- font-weight: bold;
- font-size: 1.6em;
- color: #000;
- cursor: move;
-}
-
-.dialog-body {
- overflow: auto;
- font-size: 1.3em;
- padding: 15px;
-}
-
-.dialog-instruction {
- padding: 0 0 7px;
-}
-
-.dialog-footer {
- font-size: 1.3em;
- background: #eee;
- padding: 10px;
-}
-
-.dialog-busy {
- width: 400px;
- border: none;
- -moz-border-radius: 5px;
- -webkit-border-radius: 5px;
- border-radius: 5px;
-}
-
-/**********************************************************
- * Transform Dialog
- *********************************************************/
-
-#expression-preview-tabs .ui-tabs-nav li a {
- padding: 0.15em 1em;
-}
-
-textarea.expression-preview-code {
- font-family: monospace;
- height: 5em;
- vertical-align: top;
-}
-
-.expression-preview-parsing-status {
- color: #999;
-}
-
-.expression-preview-parsing-status.error {
- color: red;
-}
-
-#expression-preview-tabs-preview,
-#expression-preview-tabs-help,
-#expression-preview-tabs-history,
-#expression-preview-tabs-starred {
- padding: 5px;
- overflow: hidden;
-}
-
-#expression-preview-tabs-preview > div,
-#expression-preview-tabs-help > div,
-#expression-preview-tabs-history > div,
-#expression-preview-tabs-starred {
- height: 200px;
- overflow: auto;
-}
-
-#expression-preview-tabs-preview td, #expression-preview-tabs-preview th,
-#expression-preview-tabs-help td, #expression-preview-tabs-help th,
-#expression-preview-tabs-history td, #expression-preview-tabs-history th,
-#expression-preview-tabs-starred td, #expression-preview-tabs-starred th {
- padding: 5px;
-}
-
-.expression-preview-table-wrapper {
- padding: 7px;
-}
-
-.expression-preview-container td {
- padding: 2px 5px;
- border-top: 1px solid #ccc;
-}
-
-td.expression-preview-heading {
- border-top: none;
- background: #ddd;
- font-weight: bold;
-}
-
-td.expression-preview-value {
- max-width: 250px !important;
- overflow-x: hidden;
-}
-
-.expression-preview-special-value {
- color: #aaa;
-}
-
-.expression-preview-help-container h3 {
- margin-top: 15px;
- margin-bottom: 7px;
- border-bottom: 1px solid #999;
-}
-
-.expression-preview-doc-item-title {
- font-weight: bold;
- text-align: right;
-}
-
-.expression-preview-doc-item-params {
-}
-
-.expression-preview-doc-item-returns {
-}
-
-.expression-preview-doc-item-desc {
- color: #666;
-}
-
-
-/**********************************************************
- * Read-only mode
- *********************************************************/
-
-.read-only .no-hidden .recline-grid tr td:first-child,
-.read-only .no-hidden .recline-grid tr th:first-child
-{
- display: none;
-}
-
-
-.read-only .write-op,
-.read-only a.data-table-cell-edit
-{
- display: none;
-}
-
diff --git a/ckan/public/scripts/vendor/recline/css/graph-flot.css b/ckan/public/scripts/vendor/recline/css/graph-flot.css
deleted file mode 100644
index 88acf5f..0000000
--- a/ckan/public/scripts/vendor/recline/css/graph-flot.css
+++ /dev/null
@@ -1,50 +0,0 @@
-.recline-graph .graph {
- height: 500px;
- margin-right: 200px;
-}
-
-.recline-graph .legend table {
- width: auto;
- margin-bottom: 0;
-}
-
-.recline-graph .legend td {
- padding: 5px;
- line-height: 13px;
-}
-
-/**********************************************************
- * Editor
- *********************************************************/
-
-.recline-graph .editor {
- float: right;
- width: 200px;
- padding-left: 0px;
-}
-
-.recline-graph .editor-info {
- padding-left: 4px;
-}
-
-.recline-graph .editor-info {
- cursor: pointer;
-}
-
-.recline-graph .editor form {
- padding-left: 4px;
-}
-
-.recline-graph .editor select {
- width: 100%;
-}
-
-.recline-graph .editor-info {
- border-bottom: 1px solid #ddd;
- margin-bottom: 10px;
-}
-
-.recline-graph .editor-hide-info p {
- display: none;
-}
-
diff --git a/ckan/public/scripts/vendor/recline/css/graph.css b/ckan/public/scripts/vendor/recline/css/graph.css
new file mode 100644
index 0000000..88acf5f
--- /dev/null
+++ b/ckan/public/scripts/vendor/recline/css/graph.css
@@ -0,0 +1,50 @@
+.recline-graph .graph {
+ height: 500px;
+ margin-right: 200px;
+}
+
+.recline-graph .legend table {
+ width: auto;
+ margin-bottom: 0;
+}
+
+.recline-graph .legend td {
+ padding: 5px;
+ line-height: 13px;
+}
+
+/**********************************************************
+ * Editor
+ *********************************************************/
+
+.recline-graph .editor {
+ float: right;
+ width: 200px;
+ padding-left: 0px;
+}
+
+.recline-graph .editor-info {
+ padding-left: 4px;
+}
+
+.recline-graph .editor-info {
+ cursor: pointer;
+}
+
+.recline-graph .editor form {
+ padding-left: 4px;
+}
+
+.recline-graph .editor select {
+ width: 100%;
+}
+
+.recline-graph .editor-info {
+ border-bottom: 1px solid #ddd;
+ margin-bottom: 10px;
+}
+
+.recline-graph .editor-hide-info p {
+ display: none;
+}
+
diff --git a/ckan/public/scripts/vendor/recline/css/grid.css b/ckan/public/scripts/vendor/recline/css/grid.css
new file mode 100644
index 0000000..aeb9984
--- /dev/null
+++ b/ckan/public/scripts/vendor/recline/css/grid.css
@@ -0,0 +1,319 @@
+/**********************************************************
+ * (Data) Grid
+ *********************************************************/
+
+.recline-grid .btn-group .dropdown-toggle {
+ padding: 1px 3px;
+ line-height: auto;
+}
+
+.recline-grid-container {
+ overflow: auto;
+ height: 550px;
+}
+
+.recline-grid {
+ border: 1px solid #ccc;
+ width: 100%;
+}
+
+.recline-grid td, .recline-grid th {
+ border-left: 1px solid #ccc;
+ padding: 3px 4px;
+ text-align: left;
+}
+
+.recline-grid td {
+ vertical-align: top;
+}
+
+.recline-grid tr td:first-child, .recline-grid tr th:first-child {
+ width: 20px;
+}
+
+/* direct borrowing from twitter buttons */
+.recline-grid th,
+.transform-column-view .expression-preview-table-wrapper th
+{
+ background-color: #e6e6e6;
+ background-repeat: no-repeat;
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));
+ background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+ background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);
+ background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+ background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+ background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+ color: #333;
+ border: 1px solid #ccc;
+ border-bottom-color: #bbb;
+ -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -webkit-transition: 0.1s linear all;
+ -moz-transition: 0.1s linear all;
+ -ms-transition: 0.1s linear all;
+ -o-transition: 0.1s linear all;
+ transition: 0.1s linear all;
+}
+
+
+/**********************************************************
+ * Data Table Menus
+ *********************************************************/
+
+.column-header-menu, a.root-header-menu {
+ float: right;
+}
+
+div.data-table-cell-content {
+ line-height: 1.2;
+ color: #222;
+ position: relative;
+}
+
+div.data-table-cell-content-numeric {
+ text-align: right;
+}
+
+a.data-table-cell-edit {
+ position: absolute;
+ top: 0;
+ right: 0;
+ display: block;
+ width: 25px;
+ height: 16px;
+ text-decoration: none;
+ background-image: url(images/edit-map.png);
+ background-repeat: no-repeat;
+ visibility: hidden;
+}
+
+a.data-table-cell-edit:hover {
+ background-position: -25px 0px;
+}
+
+.recline-grid td:hover .data-table-cell-edit {
+ visibility: visible;
+}
+
+div.data-table-cell-content-numeric > a.data-table-cell-edit {
+ left: 0px;
+ right: auto;
+}
+
+.data-table-value-nonstring {
+ color: #282;
+}
+
+.data-table-error {
+ color: red;
+}
+
+.data-table-cell-editor-editor {
+ overflow: hidden;
+ display: block;
+ width: 98%;
+ height: 3em;
+ font-family: monospace;
+ margin: 3px 0;
+}
+
+.data-table-cell-copypaste-editor {
+ overflow: hidden;
+ display: block;
+ width: 98%;
+ height: 10em;
+ font-family: monospace;
+ margin: 3px 0;
+}
+
+.data-table-cell-editor-action {
+ float: left;
+ vertical-align: bottom;
+ text-align: center;
+}
+
+.data-table-cell-editor-key {
+ font-size: 0.8em;
+ color: #999;
+}
+
+
+/**********************************************************
+ * Dialogs
+ *********************************************************/
+
+.dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #666;
+ opacity: 0.5;
+}
+
+.dialog {
+ position: fixed;
+ left: 0;
+ width: 100%;
+ text-align: center;
+}
+
+.dialog-frame {
+ margin: 0 auto;
+ text-align: left;
+ background: white;
+ border: 1px solid #3a5774;
+}
+
+.dialog-border {
+ border: 4px solid #c1d9ff;
+}
+
+.dialog-header {
+ background: #e0edfe;
+ padding: 10px;
+ font-weight: bold;
+ font-size: 1.6em;
+ color: #000;
+ cursor: move;
+}
+
+.dialog-body {
+ overflow: auto;
+ font-size: 1.3em;
+ padding: 15px;
+}
+
+.dialog-instruction {
+ padding: 0 0 7px;
+}
+
+.dialog-footer {
+ font-size: 1.3em;
+ background: #eee;
+ padding: 10px;
+}
+
+.dialog-busy {
+ width: 400px;
+ border: none;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ border-radius: 5px;
+}
+
+/**********************************************************
+ * Transform Dialog
+ *********************************************************/
+
+#expression-preview-tabs .ui-tabs-nav li a {
+ padding: 0.15em 1em;
+}
+
+textarea.expression-preview-code {
+ font-family: monospace;
+ height: 5em;
+ vertical-align: top;
+}
+
+.expression-preview-parsing-status {
+ color: #999;
+}
+
+.expression-preview-parsing-status.error {
+ color: red;
+}
+
+#expression-preview-tabs-preview,
+#expression-preview-tabs-help,
+#expression-preview-tabs-history,
+#expression-preview-tabs-starred {
+ padding: 5px;
+ overflow: hidden;
+}
+
+#expression-preview-tabs-preview > div,
+#expression-preview-tabs-help > div,
+#expression-preview-tabs-history > div,
+#expression-preview-tabs-starred {
+ height: 200px;
+ overflow: auto;
+}
+
+#expression-preview-tabs-preview td, #expression-preview-tabs-preview th,
+#expression-preview-tabs-help td, #expression-preview-tabs-help th,
+#expression-preview-tabs-history td, #expression-preview-tabs-history th,
+#expression-preview-tabs-starred td, #expression-preview-tabs-starred th {
+ padding: 5px;
+}
+
+.expression-preview-table-wrapper {
+ padding: 7px;
+}
+
+.expression-preview-container td {
+ padding: 2px 5px;
+ border-top: 1px solid #ccc;
+}
+
+td.expression-preview-heading {
+ border-top: none;
+ background: #ddd;
+ font-weight: bold;
+}
+
+td.expression-preview-value {
+ max-width: 250px !important;
+ overflow-x: hidden;
+}
+
+.expression-preview-special-value {
+ color: #aaa;
+}
+
+.expression-preview-help-container h3 {
+ margin-top: 15px;
+ margin-bottom: 7px;
+ border-bottom: 1px solid #999;
+}
+
+.expression-preview-doc-item-title {
+ font-weight: bold;
+ text-align: right;
+}
+
+.expression-preview-doc-item-params {
+}
+
+.expression-preview-doc-item-returns {
+}
+
+.expression-preview-doc-item-desc {
+ color: #666;
+}
+
+
+/**********************************************************
+ * Read-only mode
+ *********************************************************/
+
+.recline-read-only .no-hidden .recline-grid tr td:first-child,
+.recline-read-only .no-hidden .recline-grid tr th:first-child
+{
+ display: none;
+}
+
+.recline-read-only .recline-grid .write-op,
+.recline-read-only .recline-grid a.data-table-cell-edit
+{
+ display: none;
+}
+
+.recline-read-only a.row-header-menu {
+ display: none;
+}
+
diff --git a/ckan/templates/package/resource_embedded_dataviewer.html b/ckan/templates/package/resource_embedded_dataviewer.html
index 6f9b4bf..974b2f1 100644
--- a/ckan/templates/package/resource_embedded_dataviewer.html
+++ b/ckan/templates/package/resource_embedded_dataviewer.html
@@ -11,8 +11,9 @@
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.ie.css')}" />
<![endif]-->
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/data-explorer.css')}" />
- <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph-flot.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph.css')}" />
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/map.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/grid.css')}" />
<style type="text/css">
/* Hide the query controls */
diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html
index e5809c7..d16f150 100644
--- a/ckan/templates/package/resource_read.html
+++ b/ckan/templates/package/resource_read.html
@@ -20,8 +20,9 @@
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.ie.css')}" />
<![endif]-->
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/data-explorer.css')}" />
- <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph-flot.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph.css')}" />
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/map.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/grid.css')}" />
<style type="text/css">
.recline-query-editor form, .recline-query-editor .text-query {
height: 28px;
================================================================
Commit: 952306c214a171f8779207773b863ebadaf696b3
https://github.com/okfn/ckan/commit/952306c214a171f8779207773b863ebadaf696b3
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-24 (Tue, 24 Apr 2012)
Changed paths:
M ckan/public/scripts/application.js
A ckan/templates/_snippet/data-viewer-embed-dialog.html
M ckan/templates/package/resource_read.html
Log Message:
-----------
[2285] Added embeddable iframe using a modal dialog
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 637dc7b..1690982 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -1305,11 +1305,11 @@ CKAN.DataPreview = function ($, my) {
Backbone.history.start();
};
- my.makePermalink = function(explorerState) {
+ my.makeEmbedLink = function(explorerState) {
var qs = recline.View.composeQueryString({
- state: explorerState.toJSON(),
- state_version: 1
- });
+ state: explorerState.toJSON(),
+ state_version: 1
+ });
return window.location.origin + window.location.pathname + '/embed' + qs;
};
@@ -1358,11 +1358,17 @@ CKAN.DataPreview = function ($, my) {
}
});
- var permalink = $('.permalink');
- dataExplorer.state.bind('change', function() {
- permalink.attr('href', my.makePermalink(dataExplorer.state));
- });
- permalink.attr('href', my.makePermalink(dataExplorer.state));
+ var embedLink = $('.embedLink');
+ var embedIframeText = $('.embedIframeText');
+
+ function updateLink() {
+ var link = my.makeEmbedLink(dataExplorer.state);
+ embedIframeText.val($.mustache('<iframe src="{{link}}"></iframe>', {link: link.replace(/"/g, '"')}));
+ embedLink.attr('href', link);
+ }
+
+ dataExplorer.state.bind('change', updateLink);
+ updateLink();
// will have to refactor if this can get called multiple times
Backbone.history.start();
diff --git a/ckan/templates/_snippet/data-viewer-embed-dialog.html b/ckan/templates/_snippet/data-viewer-embed-dialog.html
new file mode 100644
index 0000000..eb3e577
--- /dev/null
+++ b/ckan/templates/_snippet/data-viewer-embed-dialog.html
@@ -0,0 +1,26 @@
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:i18n="http://genshi.edgewall.org/i18n"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ py:strip=""
+ >
+
+<div class="modal-data-viewer-embed-dialog modal fade in" style="display: none;"
+ py:def="data_viewer_embed_dialog()">
+ <div class="modal-header">
+ <a class="close" data-dismiss="modal">×</a>
+ <h3>
+ Embed Data Viewer
+ </h3>
+ </div>
+ <div class="modal-body">
+ <div>
+ <p><strong>Embed this view</strong> by copying this into your webpage:</p>
+ <textarea class="embedIframeText" style="width: 100%; height: 300px;">Stuff</textarea>
+ </div>
+ <a class="embedLink" href="">Preview</a>
+ </div>
+</div>
+
+</html>
diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html
index d16f150..6adb52b 100644
--- a/ckan/templates/package/resource_read.html
+++ b/ckan/templates/package/resource_read.html
@@ -12,6 +12,7 @@
py:strip="">
<xi:include href="../_snippet/data-api-help.html" />
+ <xi:include href="../_snippet/data-viewer-embed-dialog.html" />
<py:def function="optional_head">
<!-- data preview -->
@@ -59,6 +60,19 @@
.resource-actions .download img {
margin: 0px 4px -4px 0;
}
+ .preview-header {
+ padding-bottom: 13px;
+ padding-top: 0px;
+ }
+ .preview-header h3 {
+ display: inline;
+ }
+ .preview-header .btn {
+ float: right;
+ position: relative;
+ bottom: 6px;
+ padding: 8px 15px;
+ }
</style>
<script type="text/javascript">
var preload_resource = ${h.literal(c.resource_json)};
@@ -105,6 +119,8 @@
${data_api_help(c.datastore_api)}
</py:if>
+ ${data_viewer_embed_dialog()}
+
<div class="quick-info">
<dl>
<dt>Last updated</dt>
@@ -152,8 +168,10 @@
</div>
<div class="resource-preview">
- <h3>Preview</h3>
- <a class="permalink" href="">Permalink</a>
+ <div class="preview-header">
+ <h3>Preview</h3>
+ <a class="btn btn-primary" data-toggle="modal" href=".modal-data-viewer-embed-dialog">Embed</a>
+ </div>
<div id="ckanext-datapreview"></div>
</div>
================================================================
Commit: 9928c10c7f3da91d14435e992f878eb4a5161847
https://github.com/okfn/ckan/commit/9928c10c7f3da91d14435e992f878eb4a5161847
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-24 (Tue, 24 Apr 2012)
Changed paths:
M ckan/controllers/package.py
M ckan/public/scripts/application.js
M ckan/templates/_snippet/data-viewer-embed-dialog.html
M ckan/templates/package/resource_embedded_dataviewer.html
Log Message:
-----------
[2285] Configurable width and height of the embedded viewer
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index 4d2edcf..ea23594 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -772,6 +772,10 @@ def resource_embedded_dataviewer(self, id, resource_id):
c.recline_state = json.dumps(recline_state)
+ c.width = max(int(request.params.get('width', 500)), 100)
+ c.height = max(int(request.params.get('height', 500)), 100)
+ c.embedded = True
+
return render('package/resource_embedded_dataviewer.html')
def _parse_recline_state(self, state_version, raw_state):
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 1690982..da94cfe 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -1361,13 +1361,26 @@ CKAN.DataPreview = function ($, my) {
var embedLink = $('.embedLink');
var embedIframeText = $('.embedIframeText');
+ var iframeWidth = $('.iframe-width');
+ var iframeHeight = $('.iframe-height');
+
function updateLink() {
var link = my.makeEmbedLink(dataExplorer.state);
- embedIframeText.val($.mustache('<iframe src="{{link}}"></iframe>', {link: link.replace(/"/g, '"')}));
+ var width = iframeWidth.val();
+ var height = iframeHeight.val();
+ link += '&width='+width+'&height='+height;
+ embedIframeText.val($.mustache('<iframe width="{{width}}" height="{{height}}" src="{{link}}"></iframe>',
+ {
+ link: link.replace(/"/g, '"'),
+ width: width,
+ height: height
+ }));
embedLink.attr('href', link);
}
dataExplorer.state.bind('change', updateLink);
+ iframeWidth.change(updateLink);
+ iframeHeight.change(updateLink);
updateLink();
// will have to refactor if this can get called multiple times
diff --git a/ckan/templates/_snippet/data-viewer-embed-dialog.html b/ckan/templates/_snippet/data-viewer-embed-dialog.html
index eb3e577..b0b382d 100644
--- a/ckan/templates/_snippet/data-viewer-embed-dialog.html
+++ b/ckan/templates/_snippet/data-viewer-embed-dialog.html
@@ -17,7 +17,12 @@
<div class="modal-body">
<div>
<p><strong>Embed this view</strong> by copying this into your webpage:</p>
- <textarea class="embedIframeText" style="width: 100%; height: 300px;">Stuff</textarea>
+ <textarea class="embedIframeText" style="width: 100%; height: 200px;"></textarea>
+ <p>Choose width and height in pixels:</p>
+ <label for="iframe-width">Width:</label>
+ <input class="iframe-width" name="iframe-width" value="800"/>
+ <label for="iframe-height">Height:</label>
+ <input class="iframe-height" name="iframe-height" value="500"/>
</div>
<a class="embedLink" href="">Preview</a>
</div>
diff --git a/ckan/templates/package/resource_embedded_dataviewer.html b/ckan/templates/package/resource_embedded_dataviewer.html
index 974b2f1..dc54947 100644
--- a/ckan/templates/package/resource_embedded_dataviewer.html
+++ b/ckan/templates/package/resource_embedded_dataviewer.html
@@ -34,7 +34,6 @@
#main.container {
width: auto;
margin-left: 2px;
- margin-top: 2px;
}
/* Remove the border from the right-hand-side */
@@ -42,6 +41,23 @@
border: 0px;
}
+ #ckanext-datapreview {
+ width: ${c.width}px;
+ height: ${c.height}px;
+ }
+
+ .recline-grid-container {
+ height: ${c.height}px;
+ }
+
+ .recline-graph .graph {
+ height: ${c.height}px;
+ }
+
+ .recline-map .map {
+ height: ${c.height}px;
+ }
+
</style>
<script type="text/javascript">
var preload_resource = ${h.literal(c.resource_json)};
================================================================
Commit: 8eb071c70d9b2ca75022c3ff0b04cd64e115b467
https://github.com/okfn/ckan/commit/8eb071c70d9b2ca75022c3ff0b04cd64e115b467
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-24 (Tue, 24 Apr 2012)
Changed paths:
A ckan/templates/_snippet/data-viewer-embed-branded-link.html
M ckan/templates/package/resource_embedded_dataviewer.html
Log Message:
-----------
[2285] Added branded link back to resource page.
diff --git a/ckan/templates/_snippet/data-viewer-embed-branded-link.html b/ckan/templates/_snippet/data-viewer-embed-branded-link.html
new file mode 100644
index 0000000..68d3453
--- /dev/null
+++ b/ckan/templates/_snippet/data-viewer-embed-branded-link.html
@@ -0,0 +1,17 @@
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:i18n="http://genshi.edgewall.org/i18n"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ py:strip=""
+ >
+
+<p>Powered by <a href="${h.url_for(controller='package', action='resource_read', id=c.package.id, resource_id=c.resource.id)}">
+
+${g.site_title}
+<img width="64" src="${h.url_for_static(g.site_logo)}" alt="${g.site_title} Logo" title="${g.site_title} Logo" id="logo" />
+</a>
+</p>
+
+</html>
+
diff --git a/ckan/templates/package/resource_embedded_dataviewer.html b/ckan/templates/package/resource_embedded_dataviewer.html
index dc54947..7504862 100644
--- a/ckan/templates/package/resource_embedded_dataviewer.html
+++ b/ckan/templates/package/resource_embedded_dataviewer.html
@@ -74,6 +74,7 @@
<div class="resource-preview">
<div id="ckanext-datapreview"></div>
</div>
+ <xi:include href="../_snippet/data-viewer-embed-branded-link.html" />
</div>
<py:def function="optional_footer">
================================================================
Commit: 111187e4250e7425b963f747438b1ab55484e45d
https://github.com/okfn/ckan/commit/111187e4250e7425b963f747438b1ab55484e45d
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-24 (Tue, 24 Apr 2012)
Changed paths:
M ckan/public/scripts/application.js
M ckan/templates/package/resource_read.html
Log Message:
-----------
[2285] Only show embed button when the dataexplorer is available
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index da94cfe..97af2f3 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -1383,6 +1383,8 @@ CKAN.DataPreview = function ($, my) {
iframeHeight.change(updateLink);
updateLink();
+ $('.preview-header .btn').show();
+
// will have to refactor if this can get called multiple times
Backbone.history.start();
}
diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html
index 6adb52b..f9cbf4d 100644
--- a/ckan/templates/package/resource_read.html
+++ b/ckan/templates/package/resource_read.html
@@ -170,7 +170,7 @@
<div class="resource-preview">
<div class="preview-header">
<h3>Preview</h3>
- <a class="btn btn-primary" data-toggle="modal" href=".modal-data-viewer-embed-dialog">Embed</a>
+ <a style="display: none;" class="btn btn-primary" data-toggle="modal" href=".modal-data-viewer-embed-dialog">Embed</a>
</div>
<div id="ckanext-datapreview"></div>
</div>
================================================================
Commit: 7e2d796c8d6fc1966e8ba11347638e1837e32866
https://github.com/okfn/ckan/commit/7e2d796c8d6fc1966e8ba11347638e1837e32866
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-24 (Tue, 24 Apr 2012)
Changed paths:
M ckan/public/scripts/vendor/recline/css/map.css
M ckan/public/scripts/vendor/recline/recline.js
Log Message:
-----------
[2285] Pulled in latest changes from recline
diff --git a/ckan/public/scripts/vendor/recline/css/map.css b/ckan/public/scripts/vendor/recline/css/map.css
index 829d0c8..f1f2da2 100644
--- a/ckan/public/scripts/vendor/recline/css/map.css
+++ b/ckan/public/scripts/vendor/recline/css/map.css
@@ -18,6 +18,11 @@
}
.recline-map .editor select {
- width: 100%;
+ width: 100%;
}
+.recline-map .editor .editor-options {
+ margin-top: 10px;
+ border-top: 1px solid gray;
+ padding: 5px 0;
+}
diff --git a/ckan/public/scripts/vendor/recline/recline.js b/ckan/public/scripts/vendor/recline/recline.js
index e404d78..391296a 100644
--- a/ckan/public/scripts/vendor/recline/recline.js
+++ b/ckan/public/scripts/vendor/recline/recline.js
@@ -285,7 +285,8 @@ my.DocumentList = Backbone.Collection.extend({
// * format: (optional) used to indicate how the data should be formatted. For example:
// * type=date, format=yyyy-mm-dd
// * type=float, format=percentage
-// * type=float, format='###,###.##'
+// * type=string, format=link (render as hyperlink)
+// * type=string, format=markdown (render as markdown if Showdown available)
// * is_derived: (default: false) attribute indicating this field has no backend data but is just derived from other fields (see below).
//
// Following additional instance properties:
@@ -341,6 +342,22 @@ my.Field = Backbone.Model.extend({
if (format === 'percentage') {
return val + '%';
}
+ return val;
+ },
+ 'string': function(val, field, doc) {
+ var format = field.get('format');
+ if (format === 'link') {
+ return '<a href="VAL">VAL</a>'.replace(/VAL/g, val);
+ } else if (format === 'markdown') {
+ if (typeof Showdown !== 'undefined') {
+ var showdown = new Showdown.converter();
+ out = showdown.makeHtml(val);
+ return out;
+ } else {
+ return val;
+ }
+ }
+ return val;
}
}
});
@@ -1626,9 +1643,12 @@ my.Map = Backbone.View.extend({
if (!(docs instanceof Array)) docs = [docs];
+ var count = 0;
+ var wrongSoFar = 0;
_.every(docs,function(doc){
+ count += 1;
var feature = self._getGeometryFromDocument(doc);
- if (typeof feature === 'undefined'){
+ if (typeof feature === 'undefined' || feature === null){
// Empty field
return true;
} else if (feature instanceof Object){
@@ -1645,16 +1665,20 @@ my.Map = Backbone.View.extend({
feature.properties.cid = doc.cid;
try {
- self.features.addGeoJSON(feature);
+ self.features.addGeoJSON(feature);
} catch (except) {
- var msg = 'Wrong geometry value';
- if (except.message) msg += ' (' + except.message + ')';
+ wrongSoFar += 1;
+ var msg = 'Wrong geometry value';
+ if (except.message) msg += ' (' + except.message + ')';
+ if (wrongSoFar <= 10) {
my.notify(msg,{category:'error'});
- return false;
+ }
}
} else {
- my.notify('Wrong geometry value',{category:'error'});
- return false;
+ wrongSoFar += 1
+ if (wrongSoFar <= 10) {
+ my.notify('Wrong geometry value',{category:'error'});
+ }
}
return true;
});
@@ -1687,13 +1711,17 @@ my.Map = Backbone.View.extend({
return doc.attributes[this.state.get('geomField')];
} else if (this.state.get('lonField') && this.state.get('latField')){
// We'll create a GeoJSON like point object from the two lat/lon fields
- return {
- type: 'Point',
- coordinates: [
- doc.attributes[this.state.get('lonField')],
- doc.attributes[this.state.get('latField')]
- ]
- };
+ var lon = doc.get(this.state.get('lonField'));
+ var lat = doc.get(this.state.get('latField'));
+ if (lon && lat) {
+ return {
+ type: 'Point',
+ coordinates: [
+ doc.attributes[this.state.get('lonField')],
+ doc.attributes[this.state.get('latField')]
+ ]
+ };
+ }
}
return null;
}
@@ -1705,12 +1733,16 @@ my.Map = Backbone.View.extend({
// If not found, the user can define them via the UI form.
_setupGeometryField: function(){
var geomField, latField, lonField;
- this.state.set({
- geomField: this._checkField(this.geometryFieldNames),
- latField: this._checkField(this.latitudeFieldNames),
- lonField: this._checkField(this.longitudeFieldNames)
- });
this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
+ // should not overwrite if we have already set this (e.g. explicitly via state)
+ if (!this.geomReady) {
+ this.state.set({
+ geomField: this._checkField(this.geometryFieldNames),
+ latField: this._checkField(this.latitudeFieldNames),
+ lonField: this._checkField(this.longitudeFieldNames)
+ });
+ this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
+ }
},
// Private: Check if a field in the current model exists in the provided
@@ -2172,8 +2204,8 @@ my.DataExplorer = Backbone.View.extend({
initialize: function(options) {
var self = this;
this.el = $(this.el);
- // Hash of 'page' views (i.e. those for whole page) keyed by page name
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 {
@@ -2772,6 +2804,13 @@ this.recline.Backend = this.recline.Backend || {};
// backends (see recline.Model.Dataset.initialize).
__type__: 'base',
+
+ // ### readonly
+ //
+ // Class level attribute indicating that this backend is read-only (that
+ // is, cannot be written to).
+ readonly: true,
+
// ### sync
//
// An implementation of Backbone.sync that will be used to override
@@ -2832,6 +2871,32 @@ this.recline.Backend = this.recline.Backend || {};
query: function(model, queryObj) {
},
+ // ### _makeRequest
+ //
+ // Just $.ajax but in any headers in the 'headers' attribute of this
+ // Backend instance. Example:
+ //
+ // <pre>
+ // var jqxhr = this._makeRequest({
+ // url: the-url
+ // });
+ // </pre>
+ _makeRequest: function(data) {
+ var headers = this.get('headers');
+ var extras = {};
+ if (headers) {
+ extras = {
+ beforeSend: function(req) {
+ _.each(headers, function(value, key) {
+ req.setRequestHeader(key, value);
+ });
+ }
+ };
+ }
+ var data = _.extend(extras, data);
+ return $.ajax(data);
+ },
+
// convenience method to convert simple set of documents / rows to a QueryResult
_docsToQueryResult: function(rows) {
var hits = _.map(rows, function(row) {
@@ -2891,6 +2956,7 @@ this.recline.Backend = this.recline.Backend || {};
// Note that this is a **read-only** backend.
my.DataProxy = my.Base.extend({
__type__: 'dataproxy',
+ readonly: true,
defaults: {
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
},
@@ -2955,36 +3021,39 @@ this.recline.Backend = this.recline.Backend || {};
//
// Connecting to [ElasticSearch](http://www.elasticsearch.org/).
//
- // To use this backend ensure your Dataset has one of the following
- // attributes (first one found is used):
+ // Usage:
+ //
+ // <pre>
+ // var backend = new recline.Backend.ElasticSearch({
+ // // optional as can also be provided by Dataset/Document
+ // url: {url to ElasticSearch endpoint i.e. ES 'type/table' url - more info below}
+ // // optional
+ // headers: {dict of headers to add to each request}
+ // });
+ //
+ // @param {String} url: url for ElasticSearch type/table, e.g. for ES running
+ // on localhost:9200 with index // twitter and type tweet it would be:
+ //
+ // <pre>http://localhost:9200/twitter/tweet</pre>
+ //
+ // This url is optional since the ES endpoint url may be specified on the the
+ // dataset (and on a Document by the document having a dataset attribute) by
+ // having one of the following (see also `_getESUrl` function):
//
// <pre>
// elasticsearch_url
// webstore_url
// url
// </pre>
- //
- // This should point to the ES type url. E.G. for ES running on
- // localhost:9200 with index twitter and type tweet it would be
- //
- // <pre>http://localhost:9200/twitter/tweet</pre>
my.ElasticSearch = my.Base.extend({
__type__: 'elasticsearch',
- _getESUrl: function(dataset) {
- var out = dataset.get('elasticsearch_url');
- if (out) return out;
- out = dataset.get('webstore_url');
- if (out) return out;
- out = dataset.get('url');
- return out;
- },
+ readonly: false,
sync: function(method, model, options) {
var self = this;
if (method === "read") {
if (model.__type__ == 'Dataset') {
- var base = self._getESUrl(model);
- var schemaUrl = base + '/_mapping';
- var jqxhr = $.ajax({
+ var schemaUrl = self._getESUrl(model) + '/_mapping';
+ var jqxhr = this._makeRequest({
url: schemaUrl,
dataType: 'jsonp'
});
@@ -3003,11 +3072,77 @@ this.recline.Backend = this.recline.Backend || {};
dfd.reject(arguments);
});
return dfd.promise();
+ } else if (model.__type__ == 'Document') {
+ var base = this._getESUrl(model.dataset) + '/' + model.id;
+ return this._makeRequest({
+ url: base,
+ dataType: 'json'
+ });
+ }
+ } else if (method === 'update') {
+ if (model.__type__ == 'Document') {
+ return this.upsert(model.toJSON(), this._getESUrl(model.dataset));
+ }
+ } else if (method === 'delete') {
+ if (model.__type__ == 'Document') {
+ var url = this._getESUrl(model.dataset);
+ return this.delete(model.id, url);
}
- } else {
- alert('This backend currently only supports read operations');
}
},
+
+ // ### upsert
+ //
+ // create / update a document to ElasticSearch backend
+ //
+ // @param {Object} doc an object to insert to the index.
+ // @param {string} url (optional) url for ElasticSearch endpoint (if not
+ // defined called this._getESUrl()
+ upsert: function(doc, url) {
+ var data = JSON.stringify(doc);
+ url = url ? url : this._getESUrl();
+ if (doc.id) {
+ url += '/' + doc.id;
+ }
+ return this._makeRequest({
+ url: url,
+ type: 'POST',
+ data: data,
+ dataType: 'json'
+ });
+ },
+
+ // ### delete
+ //
+ // Delete a document from the ElasticSearch backend.
+ //
+ // @param {Object} id id of object to delete
+ // @param {string} url (optional) url for ElasticSearch endpoint (if not
+ // provided called this._getESUrl()
+ delete: function(id, url) {
+ url = url ? url : this._getESUrl();
+ url += '/' + id;
+ return this._makeRequest({
+ url: url,
+ type: 'DELETE',
+ dataType: 'json'
+ });
+ },
+
+ // ### _getESUrl
+ //
+ // get url to ElasticSearch endpoint (see above)
+ _getESUrl: function(dataset) {
+ if (dataset) {
+ var out = dataset.get('elasticsearch_url');
+ if (out) return out;
+ out = dataset.get('webstore_url');
+ if (out) return out;
+ out = dataset.get('url');
+ return out;
+ }
+ return this.get('url');
+ },
_normalizeQuery: function(queryObj) {
var out = queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj);
if (out.q !== undefined && out.q.trim() === '') {
@@ -3044,7 +3179,7 @@ this.recline.Backend = this.recline.Backend || {};
var queryNormalized = this._normalizeQuery(queryObj);
var data = {source: JSON.stringify(queryNormalized)};
var base = this._getESUrl(model);
- var jqxhr = $.ajax({
+ var jqxhr = this._makeRequest({
url: base + '/_search',
data: data,
dataType: 'jsonp'
@@ -3088,6 +3223,7 @@ this.recline.Backend = this.recline.Backend || {};
// </pre>
my.GDoc = my.Base.extend({
__type__: 'gdoc',
+ readonly: true,
getUrl: function(dataset) {
var url = dataset.get('url');
if (url.indexOf('feeds/list') != -1) {
@@ -3450,6 +3586,7 @@ this.recline.Backend = this.recline.Backend || {};
// </pre>
my.Memory = my.Base.extend({
__type__: 'memory',
+ readonly: false,
initialize: function() {
this.datasets = {};
},
@@ -3537,7 +3674,8 @@ this.recline.Backend = this.recline.Backend || {};
_.each(terms, function(term) {
var foundmatch = false;
dataset.fields.each(function(field) {
- var value = rawdoc[field.id].toString();
+ var value = rawdoc[field.id];
+ if (value !== null) { value = value.toString(); }
// TODO regexes?
foundmatch = foundmatch || (value === term);
// TODO: early out (once we are true should break to spare unnecessary testing)
================================================================
Commit: 9de1a32e35e877aef8d1f8b4c6f7422c2a8fd9e9
https://github.com/okfn/ckan/commit/9de1a32e35e877aef8d1f8b4c6f7422c2a8fd9e9
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-24 (Tue, 24 Apr 2012)
Changed paths:
M ckan/templates/package/resource_embedded_dataviewer.html
Log Message:
-----------
[2285] Allow the height to be smaller if possible
diff --git a/ckan/templates/package/resource_embedded_dataviewer.html b/ckan/templates/package/resource_embedded_dataviewer.html
index 7504862..c862b05 100644
--- a/ckan/templates/package/resource_embedded_dataviewer.html
+++ b/ckan/templates/package/resource_embedded_dataviewer.html
@@ -43,19 +43,23 @@
#ckanext-datapreview {
width: ${c.width}px;
- height: ${c.height}px;
+ height: auto;
+ max-height: ${c.height}px;
}
.recline-grid-container {
- height: ${c.height}px;
+ height: auto;
+ max-height: ${c.height}px;
}
.recline-graph .graph {
- height: ${c.height}px;
+ height: auto;
+ max-height: ${c.height}px;
}
.recline-map .map {
- height: ${c.height}px;
+ height: auto;
+ max-height: ${c.height}px;
}
</style>
================================================================
Commit: a0982bcb1652bcf8afdb2f74a9d517a5c5aa2e1c
https://github.com/okfn/ckan/commit/a0982bcb1652bcf8afdb2f74a9d517a5c5aa2e1c
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-24 (Tue, 24 Apr 2012)
Changed paths:
M ckan/model/package.py
M ckan/templates/package/resource_read.html
Log Message:
-----------
[2285] Disable the Embed button if the resource is private
diff --git a/ckan/model/package.py b/ckan/model/package.py
index 4365963..bf5c70d 100644
--- a/ckan/model/package.py
+++ b/ckan/model/package.py
@@ -561,6 +561,13 @@ def metadata_modified(self):
timestamp_float = timegm(timestamp_without_usecs) + usecs
return datetime.datetime.utcfromtimestamp(timestamp_float)
+ @property
+ def is_private(self):
+ """
+ A package is private if belongs to any private groups
+ """
+ return bool(self.get_groups(capacity='private'))
+
def is_in_group(self, group):
return group in self.get_groups()
diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html
index f9cbf4d..7508abb 100644
--- a/ckan/templates/package/resource_read.html
+++ b/ckan/templates/package/resource_read.html
@@ -170,7 +170,8 @@
<div class="resource-preview">
<div class="preview-header">
<h3>Preview</h3>
- <a style="display: none;" class="btn btn-primary" data-toggle="modal" href=".modal-data-viewer-embed-dialog">Embed</a>
+ <a py:if="c.pkg.is_private" title="Cannot embed as resource is private." style="display: none;" class="btn disabled" data-toggle="modal" href=".modal-data-viewer-embed-dialog">Embed</a>
+ <a py:if="not c.pkg.is_private" style="display: none;" class="btn btn-primary" data-toggle="modal" href=".modal-data-viewer-embed-dialog">Embed</a>
</div>
<div id="ckanext-datapreview"></div>
</div>
================================================================
Commit: 7211ddede488b8daafcc9e74230701611ae4182e
https://github.com/okfn/ckan/commit/7211ddede488b8daafcc9e74230701611ae4182e
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-24 (Tue, 24 Apr 2012)
Changed paths:
M ckan/controllers/package.py
M ckan/public/scripts/application.js
Log Message:
-----------
[2285] Some tidying up
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index ea23594..79db466 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -742,21 +742,13 @@ def resource_embedded_dataviewer(self, id, resource_id):
'user': c.user or c.author}
try:
- resource = get_action('resource_show')(context, {'id': resource_id})
- package = get_action('package_show')(context, {'id': id})
-
- # These are just required whilst still basing this off the resource_read
c.resource = get_action('resource_show')(context, {'id': resource_id})
c.package = get_action('package_show')(context, {'id': id})
- c.pkg = context['package']
c.resource_json = json.dumps(c.resource)
- c.pkg_dict = c.package
# double check that the resource belongs to the specified package
- if not resource['id'] in [ r['id'] for r in package['resources'] ]:
+ if not c.resource['id'] in [ r['id'] for r in c.package['resources'] ]:
raise NotFound
-
- c.resource_json = json.dumps(resource)
except NotFound:
abort(404, _('Resource not found'))
@@ -769,7 +761,7 @@ def resource_embedded_dataviewer(self, id, resource_id):
recline_state = self._parse_recline_state(state_version, raw_state)
if recline_state is None:
abort(400, ('"state" parameter must be a valid recline state (version %d)' % state_version))
-
+
c.recline_state = json.dumps(recline_state)
c.width = max(int(request.params.get('width', 500)), 100)
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 97af2f3..5774652 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -47,7 +47,7 @@ CKAN.Utils = CKAN.Utils || {};
var isEmbededDataviewer = $('body.package.resource_embedded_dataviewer').length > 0;
if (isEmbededDataviewer) {
- CKAN.DataPreview.loadPreviewDialogWithState(preload_resource, reclineState);
+ CKAN.DataPreview.loadEmbeddedPreview(preload_resource, reclineState);
}
var isDatasetNew = $('body.package.new').length > 0;
@@ -1256,14 +1256,19 @@ CKAN.DataPreview = function ($, my) {
my.dialogId = 'ckanext-datapreview';
my.$dialog = $('#' + my.dialogId);
- // **Public: Loads a data preview, taking into account an initial state**
+ // **Public: Loads a data previewer for an embedded page**
//
- my.loadPreviewDialogWithState = function(resourceData, reclineState) {
+ // Uses the provided reclineState to restore the Dataset. Creates a single
+ // view for the Dataset (the one defined by reclineState.currentView). And
+ // then passes the constructed Dataset, the constructed View, and the
+ // reclineState into the DataExplorer constructor.
+ my.loadEmbeddedPreview = function(resourceData, reclineState) {
my.$dialog.html('<h4>Loading ... <img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" class="loading-spinner" /></h4>');
+ // Restore the Dataset from the given reclineState.
var dataset = recline.Model.Dataset.restore(reclineState);
- // Only load a single view
+ // Only create the view defined in reclineState.currentView.
// TODO: tidy this up.
var views = null;
if (reclineState.currentView === 'grid') {
@@ -1295,6 +1300,7 @@ CKAN.DataPreview = function ($, my) {
}];
}
+ // Finally, construct the DataExplorer. Again, passing in the reclineState.
var dataExplorer = new recline.View.DataExplorer({
el: my.$dialog,
model: dataset,
@@ -1305,6 +1311,10 @@ CKAN.DataPreview = function ($, my) {
Backbone.history.start();
};
+ // **Public: Creates a link to the embeddable page.
+ //
+ // For a given DataExplorer state, this function constructs and returns the
+ // url to the embeddable view of the current dataexplorer state.
my.makeEmbedLink = function(explorerState) {
var qs = recline.View.composeQueryString({
state: explorerState.toJSON(),
@@ -1358,17 +1368,32 @@ CKAN.DataPreview = function ($, my) {
}
});
+ // -----------------------------
+ // Setup the Embed modal dialog.
+ // -----------------------------
+
+ // embedLink holds the url to the embeddable view of the current DataExplorer state.
var embedLink = $('.embedLink');
+
+ // embedIframeText contains the '<iframe>' construction, which sources
+ // the above link.
var embedIframeText = $('.embedIframeText');
+ // iframeWidth and iframeHeight control the width and height parameters
+ // used to construct the iframe, and are also used in the link.
var iframeWidth = $('.iframe-width');
var iframeHeight = $('.iframe-height');
+ // Update the embedLink and embedIframeText to contain the updated link
+ // and update width and height parameters.
function updateLink() {
var link = my.makeEmbedLink(dataExplorer.state);
var width = iframeWidth.val();
var height = iframeHeight.val();
link += '&width='+width+'&height='+height;
+
+ // Escape '"' characters in {{link}} in order not to prematurely close
+ // the src attribute value.
embedIframeText.val($.mustache('<iframe width="{{width}}" height="{{height}}" src="{{link}}"></iframe>',
{
link: link.replace(/"/g, '"'),
@@ -1378,11 +1403,16 @@ CKAN.DataPreview = function ($, my) {
embedLink.attr('href', link);
}
+ // Bind changes to the DataExplorer, or the two width and height inputs
+ // to re-calculate the url.
dataExplorer.state.bind('change', updateLink);
iframeWidth.change(updateLink);
iframeHeight.change(updateLink);
+
+ // Initial population of embedLink and embedIframeText
updateLink();
+ // Finally, since we have a DataExplorer, we can show the embed button.
$('.preview-header .btn').show();
// will have to refactor if this can get called multiple times
================================================================
Commit: 3606add796f1873f4fb6e61361056d5150a377cb
https://github.com/okfn/ckan/commit/3606add796f1873f4fb6e61361056d5150a377cb
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-25 (Wed, 25 Apr 2012)
Changed paths:
M ckan/templates/_snippet/data-viewer-embed-branded-link.html
M ckan/templates/package/resource_embedded_dataviewer.html
Log Message:
-----------
[2285] Fixed mistakes with iframe height.
diff --git a/ckan/templates/_snippet/data-viewer-embed-branded-link.html b/ckan/templates/_snippet/data-viewer-embed-branded-link.html
index 68d3453..87eceb6 100644
--- a/ckan/templates/_snippet/data-viewer-embed-branded-link.html
+++ b/ckan/templates/_snippet/data-viewer-embed-branded-link.html
@@ -6,12 +6,14 @@
py:strip=""
>
-<p>Powered by <a href="${h.url_for(controller='package', action='resource_read', id=c.package.id, resource_id=c.resource.id)}">
-
-${g.site_title}
-<img width="64" src="${h.url_for_static(g.site_logo)}" alt="${g.site_title} Logo" title="${g.site_title} Logo" id="logo" />
-</a>
-</p>
+<div class="branded-link">
+ <p>Powered by <a href="${h.url_for(controller='package', action='resource_read', id=c.package.id, resource_id=c.resource.id)}">
+
+ ${g.site_title}
+ <img width="64" src="${h.url_for_static(g.site_logo)}" alt="${g.site_title} Logo" title="${g.site_title} Logo" id="logo" />
+ </a>
+ </p>
+</div>
</html>
diff --git a/ckan/templates/package/resource_embedded_dataviewer.html b/ckan/templates/package/resource_embedded_dataviewer.html
index c862b05..c03f4b9 100644
--- a/ckan/templates/package/resource_embedded_dataviewer.html
+++ b/ckan/templates/package/resource_embedded_dataviewer.html
@@ -42,24 +42,28 @@
}
#ckanext-datapreview {
- width: ${c.width}px;
- height: auto;
- max-height: ${c.height}px;
+ width: ${c.width-2}px;
+ height: ${c.height-115}px;
}
.recline-grid-container {
- height: auto;
- max-height: ${c.height}px;
+ height: ${c.height-115}px;
}
.recline-graph .graph {
- height: auto;
- max-height: ${c.height}px;
+ height: ${c.height-115}px;
}
.recline-map .map {
- height: auto;
- max-height: ${c.height}px;
+ height: ${c.height-115}px;
+ }
+
+ .branded-link {
+ height: 70px;
+ }
+
+ .alert-messages {
+ display: none;
}
</style>
================================================================
Commit: 98b3e240e470563ff59e1ec8717f48a56c48f4ef
https://github.com/okfn/ckan/commit/98b3e240e470563ff59e1ec8717f48a56c48f4ef
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-25 (Wed, 25 Apr 2012)
Changed paths:
M ckan/public/scripts/application.js
Log Message:
-----------
[2285] Escape the dataset url.
Strictly, everything should be escaped, but in order to have legible looking urls, it's enough
to just escape the dataset url, at least for now.
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 5774652..462e774 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -1316,6 +1316,8 @@ CKAN.DataPreview = function ($, my) {
// For a given DataExplorer state, this function constructs and returns the
// url to the embeddable view of the current dataexplorer state.
my.makeEmbedLink = function(explorerState) {
+ var state = explorerState.toJSON();
+ state.dataset.url = escape(state.dataset.url);
var qs = recline.View.composeQueryString({
state: explorerState.toJSON(),
state_version: 1
================================================================
Commit: 8c267d296cfa09ff1e6f7572167021bff3f6a0c5
https://github.com/okfn/ckan/commit/8c267d296cfa09ff1e6f7572167021bff3f6a0c5
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-25 (Wed, 25 Apr 2012)
Changed paths:
M ckan/public/scripts/application.js
Log Message:
-----------
[2285] Bind to changes on the data explorer's views
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 462e774..d78c3a9 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -1408,6 +1408,10 @@ CKAN.DataPreview = function ($, my) {
// Bind changes to the DataExplorer, or the two width and height inputs
// to re-calculate the url.
dataExplorer.state.bind('change', updateLink);
+ for (var i=0; i<dataExplorer.pageViews.length; i++) {
+ dataExplorer.pageViews[i].view.state.bind('change', updateLink);
+ }
+
iframeWidth.change(updateLink);
iframeHeight.change(updateLink);
================================================================
Commit: 0fc677d3c7af770d4caa93572681bc7455233e00
https://github.com/okfn/ckan/commit/0fc677d3c7af770d4caa93572681bc7455233e00
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-25 (Wed, 25 Apr 2012)
Changed paths:
M ckan/public/scripts/vendor/recline/recline.js
Log Message:
-----------
[2285] Pulled in latest changes in recline
diff --git a/ckan/public/scripts/vendor/recline/recline.js b/ckan/public/scripts/vendor/recline/recline.js
index 391296a..c1f9185 100644
--- a/ckan/public/scripts/vendor/recline/recline.js
+++ b/ckan/public/scripts/vendor/recline/recline.js
@@ -1404,7 +1404,7 @@ this.recline.View = this.recline.View || {};
//
// <pre>
// {
-// // geomField if specified will be used in preference to lat/lon
+// // geomField if specified will be used in preference to lat/lon
// geomField: {id of field containing geometry in the dataset}
// lonField: {id of field containing longitude in the dataset}
// latField: {id of field containing latitude in the dataset}
@@ -1462,6 +1462,11 @@ my.Map = Backbone.View.extend({
<div class="editor-buttons"> \
<button class="btn editor-update-map">Update</button> \
</div> \
+ <div class="editor-options" > \
+ <label class="checkbox"> \
+ <input type="checkbox" id="editor-auto-zoom" checked="checked" /> \
+ Auto zoom to features</label> \
+ </div> \
<input type="hidden" class="editor-id" value="map-1" /> \
</div> \
</form> \
@@ -1479,7 +1484,8 @@ my.Map = Backbone.View.extend({
// Define here events for UI elements
events: {
'click .editor-update-map': 'onEditorSubmit',
- 'change .editor-field-type': 'onFieldTypeChange'
+ 'change .editor-field-type': 'onFieldTypeChange',
+ 'change #editor-auto-zoom': 'onAutoZoomChange'
},
initialize: function(options) {
@@ -1498,15 +1504,27 @@ my.Map = Backbone.View.extend({
// Listen to changes in the documents
this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
+ this.model.currentDocuments.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')});
- // If the div was hidden, Leaflet needs to recalculate some sizes
- // to display properly
this.bind('view:show',function(){
- if (self.map) {
- self.map.invalidateSize();
+ // If the div was hidden, Leaflet needs to recalculate some sizes
+ // to display properly
+ if (self.map){
+ self.map.invalidateSize();
+ if (self._zoomPending && self.autoZoom) {
+ self._zoomToFeatures();
+ self._zoomPending = false;
}
+ }
+ self.visible = true;
+ });
+ this.bind('view:hide',function(){
+ self.visible = false;
});
var stateData = _.extend({
@@ -1518,6 +1536,7 @@ my.Map = Backbone.View.extend({
);
this.state = new recline.Model.ObjectState(stateData);
+ this.autoZoom = true;
this.mapReady = false;
this.render();
},
@@ -1583,6 +1602,13 @@ my.Map = Backbone.View.extend({
this.features.clearLayers();
this._add(this.model.currentDocuments.models);
}
+ if (action != 'reset' && this.autoZoom){
+ if (this.visible){
+ this._zoomToFeatures();
+ } else {
+ this._zoomPending = true;
+ }
+ }
}
},
@@ -1629,6 +1655,10 @@ my.Map = Backbone.View.extend({
}
},
+ onAutoZoomChange: function(e){
+ this.autoZoom = !this.autoZoom;
+ },
+
// Private: Add one or n features to the map
//
// For each document passed, a GeoJSON geometry will be extracted and added
@@ -1656,7 +1686,9 @@ my.Map = Backbone.View.extend({
// TODO: mustache?
html = ''
for (key in doc.attributes){
- html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>'
+ if (!(self.state.get('geomField') && key == self.state.get('geomField'))){
+ html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>';
+ }
}
feature.properties = {popupContent: html};
@@ -1707,19 +1739,22 @@ my.Map = Backbone.View.extend({
_getGeometryFromDocument: function(doc){
if (this.geomReady){
if (this.state.get('geomField')){
- // We assume that the contents of the field are a valid GeoJSON object
- return doc.attributes[this.state.get('geomField')];
+ var value = doc.get(this.state.get('geomField'));
+ if (typeof(value) === 'string'){
+ // We have a GeoJSON string representation
+ return $.parseJSON(value);
+ } else {
+ // We assume that the contents of the field are a valid GeoJSON object
+ return value;
+ }
} else if (this.state.get('lonField') && this.state.get('latField')){
// We'll create a GeoJSON like point object from the two lat/lon fields
var lon = doc.get(this.state.get('lonField'));
var lat = doc.get(this.state.get('latField'));
- if (lon && lat) {
+ if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
return {
type: 'Point',
- coordinates: [
- doc.attributes[this.state.get('lonField')],
- doc.attributes[this.state.get('latField')]
- ]
+ coordinates: [lon,lat]
};
}
}
@@ -1761,6 +1796,18 @@ my.Map = Backbone.View.extend({
return null;
},
+ // Private: Zoom to map to current features extent if any, or to the full
+ // extent if none.
+ //
+ _zoomToFeatures: function(){
+ var bounds = this.features.getBounds();
+ if (bounds){
+ this.map.fitBounds(bounds);
+ } else {
+ this.map.setView(new L.LatLng(0, 0), 2);
+ }
+ },
+
// Private: Sets up the Leaflet map control and the features layer.
//
// The map uses a base layer from [MapQuest](http://www.mapquest.com) based
@@ -1785,6 +1832,24 @@ my.Map = Backbone.View.extend({
}
});
+
+ // This will be available in the next Leaflet stable release.
+ // In the meantime we add it manually to our layer.
+ this.features.getBounds = function(){
+ var bounds = new L.LatLngBounds();
+ this._iterateLayers(function (layer) {
+ if (layer instanceof L.Marker){
+ bounds.extend(layer.getLatLng());
+ } else {
+ if (layer.getBounds){
+ bounds.extend(layer.getBounds().getNorthEast());
+ bounds.extend(layer.getBounds().getSouthWest());
+ }
+ }
+ }, this);
+ return (typeof bounds.getNorthEast() !== 'undefined') ? bounds : null;
+ }
+
this.map.addLayer(this.features);
this.map.setView(new L.LatLng(0, 0), 2);
@@ -3404,7 +3469,9 @@ this.recline.Backend = this.recline.Backend || {};
var options = options || {};
var trm = options.trim;
var separator = options.separator || ',';
-
+ var delimiter = options.delimiter || '"';
+
+
var cur = '', // The character we are currently processing.
inQuote = false,
fieldQuoted = false,
@@ -3451,8 +3518,8 @@ this.recline.Backend = this.recline.Backend || {};
field = '';
fieldQuoted = false;
} else {
- // If it's not a ", add it to the field buffer
- if (cur !== '"') {
+ // If it's not a delimiter, add it to the field buffer
+ if (cur !== delimiter) {
field += cur;
} else {
if (!inQuote) {
@@ -3460,9 +3527,9 @@ this.recline.Backend = this.recline.Backend || {};
inQuote = true;
fieldQuoted = true;
} else {
- // Next char is ", this is an escaped "
- if (s.charAt(i + 1) === '"') {
- field += '"';
+ // Next char is delimiter, this is an escaped delimiter
+ if (s.charAt(i + 1) === delimiter) {
+ field += delimiter;
// Skip the next char
i += 1;
} else {
================================================================
Commit: d9fd8dfa0d03610e9c2d8387b29dd2af4f7b258b
https://github.com/okfn/ckan/commit/d9fd8dfa0d03610e9c2d8387b29dd2af4f7b258b
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-25 (Wed, 25 Apr 2012)
Changed paths:
M ckan/public/scripts/application.js
M ckan/templates/package/resource_read.html
Log Message:
-----------
[2285] Cross-browser compatibible way of providing embed src lnk
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index d78c3a9..46456c2 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -1322,7 +1322,7 @@ CKAN.DataPreview = function ($, my) {
state: explorerState.toJSON(),
state_version: 1
});
- return window.location.origin + window.location.pathname + '/embed' + qs;
+ return embedPath + qs;
};
// **Public: Loads a data preview**
diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html
index 7508abb..c99805a 100644
--- a/ckan/templates/package/resource_read.html
+++ b/ckan/templates/package/resource_read.html
@@ -76,6 +76,7 @@
</style>
<script type="text/javascript">
var preload_resource = ${h.literal(c.resource_json)};
+ var embedPath = "${g.site_url+h.url_for(controller='package', action='resource_embedded_dataviewer', id=c.package.id, resource_id=c.resource.id)}";
</script>
</py:def>
================================================================
Commit: 2821abf2a92182f590219d78f75e6ab969ab555a
https://github.com/okfn/ckan/commit/2821abf2a92182f590219d78f75e6ab969ab555a
Author: Ian Murray <ian.murray at okfn.org>
Date: 2012-04-25 (Wed, 25 Apr 2012)
Changed paths:
M ckan/config/routing.py
M ckan/controllers/package.py
M ckan/model/package.py
M ckan/public/scripts/application.js
M ckan/public/scripts/vendor/recline/css/data-explorer.css
R ckan/public/scripts/vendor/recline/css/graph-flot.css
A ckan/public/scripts/vendor/recline/css/graph.css
A ckan/public/scripts/vendor/recline/css/grid.css
M ckan/public/scripts/vendor/recline/css/map.css
M ckan/public/scripts/vendor/recline/recline.js
A ckan/templates/_snippet/data-viewer-embed-branded-link.html
A ckan/templates/_snippet/data-viewer-embed-dialog.html
A ckan/templates/package/resource_embedded_dataviewer.html
M ckan/templates/package/resource_read.html
Log Message:
-----------
Merge branch 'feature-2285-embeddable-data-viewer'
diff --git a/ckan/config/routing.py b/ckan/config/routing.py
index d67727d..6c03cb0 100644
--- a/ckan/config/routing.py
+++ b/ckan/config/routing.py
@@ -189,6 +189,7 @@ def make_map():
m.connect('/dataset/{id}.{format}', action='read')
m.connect('/dataset/{id}', action='read')
m.connect('/dataset/{id}/resource/{resource_id}', action='resource_read')
+ m.connect('/dataset/{id}/resource/{resource_id}/embed', action='resource_embedded_dataviewer')
# group
map.redirect('/groups', '/group')
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index 37d5b31..08656ae 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -751,3 +751,59 @@ def resource_read(self, id, resource_id):
qualified=True)
return render('package/resource_read.html')
+ def resource_embedded_dataviewer(self, id, resource_id):
+ """
+ Embeded page for a read-only resource dataview.
+ """
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author}
+
+ try:
+ c.resource = get_action('resource_show')(context, {'id': resource_id})
+ c.package = get_action('package_show')(context, {'id': id})
+ c.resource_json = json.dumps(c.resource)
+
+ # double check that the resource belongs to the specified package
+ if not c.resource['id'] in [ r['id'] for r in c.package['resources'] ]:
+ raise NotFound
+
+ except NotFound:
+ abort(404, _('Resource not found'))
+ except NotAuthorized:
+ abort(401, _('Unauthorized to read resource %s') % id)
+
+ # Construct the recline state
+ state_version = int(request.params.get('state_version', '1'))
+ raw_state = request.params.get('state', '')
+ recline_state = self._parse_recline_state(state_version, raw_state)
+ if recline_state is None:
+ abort(400, ('"state" parameter must be a valid recline state (version %d)' % state_version))
+
+ c.recline_state = json.dumps(recline_state)
+
+ c.width = max(int(request.params.get('width', 500)), 100)
+ c.height = max(int(request.params.get('height', 500)), 100)
+ c.embedded = True
+
+ return render('package/resource_embedded_dataviewer.html')
+
+ def _parse_recline_state(self, state_version, raw_state):
+ if state_version != 1: # Only support one version at the moment
+ return None
+
+ try:
+ state = json.loads(raw_state)
+ except ValueError:
+ return None
+
+ # Ensure the state is readOnly
+ state['readOnly'] = True
+
+ # Ensure only the currentView is available
+ if not state.get('currentView', None):
+ state['currentView'] = 'grid' # default to grid view if none specified
+ for k in state.keys():
+ if k.startswith('view-') and not k.endswith(state['currentView']):
+ state.pop(k)
+
+ return state
diff --git a/ckan/model/package.py b/ckan/model/package.py
index dd1e0d3..3ba9cb0 100644
--- a/ckan/model/package.py
+++ b/ckan/model/package.py
@@ -562,6 +562,13 @@ def metadata_modified(self):
timestamp_float = timegm(timestamp_without_usecs) + usecs
return datetime.datetime.utcfromtimestamp(timestamp_float)
+ @property
+ def is_private(self):
+ """
+ A package is private if belongs to any private groups
+ """
+ return bool(self.get_groups(capacity='private'))
+
def is_in_group(self, group):
return group in self.get_groups()
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 1c12a81..9cd02ce 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -45,6 +45,12 @@ CKAN.Utils = CKAN.Utils || {};
if (isResourceView) {
CKAN.DataPreview.loadPreviewDialog(preload_resource);
}
+
+ var isEmbededDataviewer = $('body.package.resource_embedded_dataviewer').length > 0;
+ if (isEmbededDataviewer) {
+ CKAN.DataPreview.loadEmbeddedPreview(preload_resource, reclineState);
+ }
+
var isDatasetNew = $('body.package.new').length > 0;
if (isDatasetNew) {
// Set up magic URL slug editor
@@ -1344,6 +1350,75 @@ CKAN.DataPreview = function ($, my) {
my.dialogId = 'ckanext-datapreview';
my.$dialog = $('#' + my.dialogId);
+ // **Public: Loads a data previewer for an embedded page**
+ //
+ // Uses the provided reclineState to restore the Dataset. Creates a single
+ // view for the Dataset (the one defined by reclineState.currentView). And
+ // then passes the constructed Dataset, the constructed View, and the
+ // reclineState into the DataExplorer constructor.
+ my.loadEmbeddedPreview = function(resourceData, reclineState) {
+ my.$dialog.html('<h4>Loading ... <img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" class="loading-spinner" /></h4>');
+
+ // Restore the Dataset from the given reclineState.
+ var dataset = recline.Model.Dataset.restore(reclineState);
+
+ // Only create the view defined in reclineState.currentView.
+ // TODO: tidy this up.
+ var views = null;
+ if (reclineState.currentView === 'grid') {
+ views = [ {
+ id: 'grid',
+ label: 'Grid',
+ view: new recline.View.Grid({
+ model: dataset,
+ state: reclineState['view-grid']
+ })
+ }];
+ } else if (reclineState.currentView === 'graph') {
+ views = [ {
+ id: 'graph',
+ label: 'Graph',
+ view: new recline.View.Graph({
+ model: dataset,
+ state: reclineState['view-graph']
+ })
+ }];
+ } else if (reclineState.currentView === 'map') {
+ views = [ {
+ id: 'map',
+ label: 'Map',
+ view: new recline.View.Map({
+ model: dataset,
+ state: reclineState['view-map']
+ })
+ }];
+ }
+
+ // Finally, construct the DataExplorer. Again, passing in the reclineState.
+ var dataExplorer = new recline.View.DataExplorer({
+ el: my.$dialog,
+ model: dataset,
+ state: reclineState,
+ views: views
+ });
+
+ Backbone.history.start();
+ };
+
+ // **Public: Creates a link to the embeddable page.
+ //
+ // For a given DataExplorer state, this function constructs and returns the
+ // url to the embeddable view of the current dataexplorer state.
+ my.makeEmbedLink = function(explorerState) {
+ var state = explorerState.toJSON();
+ state.dataset.url = escape(state.dataset.url);
+ var qs = recline.View.composeQueryString({
+ state: explorerState.toJSON(),
+ state_version: 1
+ });
+ return embedPath + qs;
+ };
+
// **Public: Loads a data preview**
//
// Fetches the preview data object from the link provided and loads the
@@ -1361,14 +1436,14 @@ CKAN.DataPreview = function ($, my) {
{
id: 'grid',
label: 'Grid',
- view: new recline.View.DataGrid({
+ view: new recline.View.Grid({
model: dataset
})
},
{
id: 'graph',
label: 'Graph',
- view: new recline.View.FlotGraph({
+ view: new recline.View.Graph({
model: dataset
})
},
@@ -1388,6 +1463,58 @@ CKAN.DataPreview = function ($, my) {
readOnly: true
}
});
+
+ // -----------------------------
+ // Setup the Embed modal dialog.
+ // -----------------------------
+
+ // embedLink holds the url to the embeddable view of the current DataExplorer state.
+ var embedLink = $('.embedLink');
+
+ // embedIframeText contains the '<iframe>' construction, which sources
+ // the above link.
+ var embedIframeText = $('.embedIframeText');
+
+ // iframeWidth and iframeHeight control the width and height parameters
+ // used to construct the iframe, and are also used in the link.
+ var iframeWidth = $('.iframe-width');
+ var iframeHeight = $('.iframe-height');
+
+ // Update the embedLink and embedIframeText to contain the updated link
+ // and update width and height parameters.
+ function updateLink() {
+ var link = my.makeEmbedLink(dataExplorer.state);
+ var width = iframeWidth.val();
+ var height = iframeHeight.val();
+ link += '&width='+width+'&height='+height;
+
+ // Escape '"' characters in {{link}} in order not to prematurely close
+ // the src attribute value.
+ embedIframeText.val($.mustache('<iframe width="{{width}}" height="{{height}}" src="{{link}}"></iframe>',
+ {
+ link: link.replace(/"/g, '"'),
+ width: width,
+ height: height
+ }));
+ embedLink.attr('href', link);
+ }
+
+ // Bind changes to the DataExplorer, or the two width and height inputs
+ // to re-calculate the url.
+ dataExplorer.state.bind('change', updateLink);
+ for (var i=0; i<dataExplorer.pageViews.length; i++) {
+ dataExplorer.pageViews[i].view.state.bind('change', updateLink);
+ }
+
+ iframeWidth.change(updateLink);
+ iframeHeight.change(updateLink);
+
+ // Initial population of embedLink and embedIframeText
+ updateLink();
+
+ // Finally, since we have a DataExplorer, we can show the embed button.
+ $('.preview-header .btn').show();
+
// will have to refactor if this can get called multiple times
Backbone.history.start();
}
diff --git a/ckan/public/scripts/vendor/recline/css/data-explorer.css b/ckan/public/scripts/vendor/recline/css/data-explorer.css
index 6cc5b0a..766a91e 100644
--- a/ckan/public/scripts/vendor/recline/css/data-explorer.css
+++ b/ckan/public/scripts/vendor/recline/css/data-explorer.css
@@ -78,320 +78,3 @@
display: inline-block;
}
-
-/**********************************************************
- * Data Table
- *********************************************************/
-
-.recline-grid .btn-group .dropdown-toggle {
- padding: 1px 3px;
- line-height: auto;
-}
-
-.recline-grid-container {
- overflow: auto;
- height: 550px;
-}
-
-.recline-grid {
- border: 1px solid #ccc;
- width: 100%;
-}
-
-.recline-grid td, .recline-grid th {
- border-left: 1px solid #ccc;
- padding: 3px 4px;
- text-align: left;
-}
-
-.recline-grid tr td:first-child, .recline-grid tr th:first-child {
- width: 20px;
-}
-
-/* direct borrowing from twitter buttons */
-.recline-grid th,
-.transform-column-view .expression-preview-table-wrapper th
-{
- background-color: #e6e6e6;
- background-repeat: no-repeat;
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));
- background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
- background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);
- background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
- background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
- background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);
- text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
- color: #333;
- border: 1px solid #ccc;
- border-bottom-color: #bbb;
- -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
- -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
- -webkit-transition: 0.1s linear all;
- -moz-transition: 0.1s linear all;
- -ms-transition: 0.1s linear all;
- -o-transition: 0.1s linear all;
- transition: 0.1s linear all;
-}
-
-
-/**********************************************************
- * Data Table Menus
- *********************************************************/
-
-.column-header-menu, a.root-header-menu {
- float: right;
-}
-
-.read-only a.row-header-menu {
- display: none;
-}
-
-div.data-table-cell-content {
- line-height: 1.2;
- color: #222;
- position: relative;
-}
-
-div.data-table-cell-content-numeric {
- text-align: right;
-}
-
-a.data-table-cell-edit {
- position: absolute;
- top: 0;
- right: 0;
- display: block;
- width: 25px;
- height: 16px;
- text-decoration: none;
- background-image: url(images/edit-map.png);
- background-repeat: no-repeat;
- visibility: hidden;
-}
-
-a.data-table-cell-edit:hover {
- background-position: -25px 0px;
-}
-
-.recline-grid td:hover .data-table-cell-edit {
- visibility: visible;
-}
-
-div.data-table-cell-content-numeric > a.data-table-cell-edit {
- left: 0px;
- right: auto;
-}
-
-.data-table-value-nonstring {
- color: #282;
-}
-
-.data-table-error {
- color: red;
-}
-
-.data-table-cell-editor-editor {
- overflow: hidden;
- display: block;
- width: 98%;
- height: 3em;
- font-family: monospace;
- margin: 3px 0;
-}
-
-.data-table-cell-copypaste-editor {
- overflow: hidden;
- display: block;
- width: 98%;
- height: 10em;
- font-family: monospace;
- margin: 3px 0;
-}
-
-.data-table-cell-editor-action {
- float: left;
- vertical-align: bottom;
- text-align: center;
-}
-
-.data-table-cell-editor-key {
- font-size: 0.8em;
- color: #999;
-}
-
-
-/**********************************************************
- * Dialogs
- *********************************************************/
-
-.dialog-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: #666;
- opacity: 0.5;
-}
-
-.dialog {
- position: fixed;
- left: 0;
- width: 100%;
- text-align: center;
-}
-
-.dialog-frame {
- margin: 0 auto;
- text-align: left;
- background: white;
- border: 1px solid #3a5774;
-}
-
-.dialog-border {
- border: 4px solid #c1d9ff;
-}
-
-.dialog-header {
- background: #e0edfe;
- padding: 10px;
- font-weight: bold;
- font-size: 1.6em;
- color: #000;
- cursor: move;
-}
-
-.dialog-body {
- overflow: auto;
- font-size: 1.3em;
- padding: 15px;
-}
-
-.dialog-instruction {
- padding: 0 0 7px;
-}
-
-.dialog-footer {
- font-size: 1.3em;
- background: #eee;
- padding: 10px;
-}
-
-.dialog-busy {
- width: 400px;
- border: none;
- -moz-border-radius: 5px;
- -webkit-border-radius: 5px;
- border-radius: 5px;
-}
-
-/**********************************************************
- * Transform Dialog
- *********************************************************/
-
-#expression-preview-tabs .ui-tabs-nav li a {
- padding: 0.15em 1em;
-}
-
-textarea.expression-preview-code {
- font-family: monospace;
- height: 5em;
- vertical-align: top;
-}
-
-.expression-preview-parsing-status {
- color: #999;
-}
-
-.expression-preview-parsing-status.error {
- color: red;
-}
-
-#expression-preview-tabs-preview,
-#expression-preview-tabs-help,
-#expression-preview-tabs-history,
-#expression-preview-tabs-starred {
- padding: 5px;
- overflow: hidden;
-}
-
-#expression-preview-tabs-preview > div,
-#expression-preview-tabs-help > div,
-#expression-preview-tabs-history > div,
-#expression-preview-tabs-starred {
- height: 200px;
- overflow: auto;
-}
-
-#expression-preview-tabs-preview td, #expression-preview-tabs-preview th,
-#expression-preview-tabs-help td, #expression-preview-tabs-help th,
-#expression-preview-tabs-history td, #expression-preview-tabs-history th,
-#expression-preview-tabs-starred td, #expression-preview-tabs-starred th {
- padding: 5px;
-}
-
-.expression-preview-table-wrapper {
- padding: 7px;
-}
-
-.expression-preview-container td {
- padding: 2px 5px;
- border-top: 1px solid #ccc;
-}
-
-td.expression-preview-heading {
- border-top: none;
- background: #ddd;
- font-weight: bold;
-}
-
-td.expression-preview-value {
- max-width: 250px !important;
- overflow-x: hidden;
-}
-
-.expression-preview-special-value {
- color: #aaa;
-}
-
-.expression-preview-help-container h3 {
- margin-top: 15px;
- margin-bottom: 7px;
- border-bottom: 1px solid #999;
-}
-
-.expression-preview-doc-item-title {
- font-weight: bold;
- text-align: right;
-}
-
-.expression-preview-doc-item-params {
-}
-
-.expression-preview-doc-item-returns {
-}
-
-.expression-preview-doc-item-desc {
- color: #666;
-}
-
-
-/**********************************************************
- * Read-only mode
- *********************************************************/
-
-.read-only .no-hidden .recline-grid tr td:first-child,
-.read-only .no-hidden .recline-grid tr th:first-child
-{
- display: none;
-}
-
-
-.read-only .write-op,
-.read-only a.data-table-cell-edit
-{
- display: none;
-}
-
diff --git a/ckan/public/scripts/vendor/recline/css/graph-flot.css b/ckan/public/scripts/vendor/recline/css/graph-flot.css
deleted file mode 100644
index d50f11e..0000000
--- a/ckan/public/scripts/vendor/recline/css/graph-flot.css
+++ /dev/null
@@ -1,50 +0,0 @@
-.data-graph-container .graph {
- height: 500px;
- margin-right: 200px;
-}
-
-.data-graph-container .legend table {
- width: auto;
- margin-bottom: 0;
-}
-
-.data-graph-container .legend td {
- padding: 5px;
- line-height: 13px;
-}
-
-/**********************************************************
- * Editor
- *********************************************************/
-
-.data-graph-container .editor {
- float: right;
- width: 200px;
- padding-left: 0px;
-}
-
-.data-graph-container .editor-info {
- padding-left: 4px;
-}
-
-.data-graph-container .editor-info {
- cursor: pointer;
-}
-
-.data-graph-container .editor form {
- padding-left: 4px;
-}
-
-.data-graph-container .editor select {
- width: 100%;
-}
-
-.data-graph-container .editor-info {
- border-bottom: 1px solid #ddd;
- margin-bottom: 10px;
-}
-
-.data-graph-container .editor-hide-info p {
- display: none;
-}
-
diff --git a/ckan/public/scripts/vendor/recline/css/graph.css b/ckan/public/scripts/vendor/recline/css/graph.css
new file mode 100644
index 0000000..88acf5f
--- /dev/null
+++ b/ckan/public/scripts/vendor/recline/css/graph.css
@@ -0,0 +1,50 @@
+.recline-graph .graph {
+ height: 500px;
+ margin-right: 200px;
+}
+
+.recline-graph .legend table {
+ width: auto;
+ margin-bottom: 0;
+}
+
+.recline-graph .legend td {
+ padding: 5px;
+ line-height: 13px;
+}
+
+/**********************************************************
+ * Editor
+ *********************************************************/
+
+.recline-graph .editor {
+ float: right;
+ width: 200px;
+ padding-left: 0px;
+}
+
+.recline-graph .editor-info {
+ padding-left: 4px;
+}
+
+.recline-graph .editor-info {
+ cursor: pointer;
+}
+
+.recline-graph .editor form {
+ padding-left: 4px;
+}
+
+.recline-graph .editor select {
+ width: 100%;
+}
+
+.recline-graph .editor-info {
+ border-bottom: 1px solid #ddd;
+ margin-bottom: 10px;
+}
+
+.recline-graph .editor-hide-info p {
+ display: none;
+}
+
diff --git a/ckan/public/scripts/vendor/recline/css/grid.css b/ckan/public/scripts/vendor/recline/css/grid.css
new file mode 100644
index 0000000..aeb9984
--- /dev/null
+++ b/ckan/public/scripts/vendor/recline/css/grid.css
@@ -0,0 +1,319 @@
+/**********************************************************
+ * (Data) Grid
+ *********************************************************/
+
+.recline-grid .btn-group .dropdown-toggle {
+ padding: 1px 3px;
+ line-height: auto;
+}
+
+.recline-grid-container {
+ overflow: auto;
+ height: 550px;
+}
+
+.recline-grid {
+ border: 1px solid #ccc;
+ width: 100%;
+}
+
+.recline-grid td, .recline-grid th {
+ border-left: 1px solid #ccc;
+ padding: 3px 4px;
+ text-align: left;
+}
+
+.recline-grid td {
+ vertical-align: top;
+}
+
+.recline-grid tr td:first-child, .recline-grid tr th:first-child {
+ width: 20px;
+}
+
+/* direct borrowing from twitter buttons */
+.recline-grid th,
+.transform-column-view .expression-preview-table-wrapper th
+{
+ background-color: #e6e6e6;
+ background-repeat: no-repeat;
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));
+ background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+ background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);
+ background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+ background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+ background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+ color: #333;
+ border: 1px solid #ccc;
+ border-bottom-color: #bbb;
+ -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -webkit-transition: 0.1s linear all;
+ -moz-transition: 0.1s linear all;
+ -ms-transition: 0.1s linear all;
+ -o-transition: 0.1s linear all;
+ transition: 0.1s linear all;
+}
+
+
+/**********************************************************
+ * Data Table Menus
+ *********************************************************/
+
+.column-header-menu, a.root-header-menu {
+ float: right;
+}
+
+div.data-table-cell-content {
+ line-height: 1.2;
+ color: #222;
+ position: relative;
+}
+
+div.data-table-cell-content-numeric {
+ text-align: right;
+}
+
+a.data-table-cell-edit {
+ position: absolute;
+ top: 0;
+ right: 0;
+ display: block;
+ width: 25px;
+ height: 16px;
+ text-decoration: none;
+ background-image: url(images/edit-map.png);
+ background-repeat: no-repeat;
+ visibility: hidden;
+}
+
+a.data-table-cell-edit:hover {
+ background-position: -25px 0px;
+}
+
+.recline-grid td:hover .data-table-cell-edit {
+ visibility: visible;
+}
+
+div.data-table-cell-content-numeric > a.data-table-cell-edit {
+ left: 0px;
+ right: auto;
+}
+
+.data-table-value-nonstring {
+ color: #282;
+}
+
+.data-table-error {
+ color: red;
+}
+
+.data-table-cell-editor-editor {
+ overflow: hidden;
+ display: block;
+ width: 98%;
+ height: 3em;
+ font-family: monospace;
+ margin: 3px 0;
+}
+
+.data-table-cell-copypaste-editor {
+ overflow: hidden;
+ display: block;
+ width: 98%;
+ height: 10em;
+ font-family: monospace;
+ margin: 3px 0;
+}
+
+.data-table-cell-editor-action {
+ float: left;
+ vertical-align: bottom;
+ text-align: center;
+}
+
+.data-table-cell-editor-key {
+ font-size: 0.8em;
+ color: #999;
+}
+
+
+/**********************************************************
+ * Dialogs
+ *********************************************************/
+
+.dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #666;
+ opacity: 0.5;
+}
+
+.dialog {
+ position: fixed;
+ left: 0;
+ width: 100%;
+ text-align: center;
+}
+
+.dialog-frame {
+ margin: 0 auto;
+ text-align: left;
+ background: white;
+ border: 1px solid #3a5774;
+}
+
+.dialog-border {
+ border: 4px solid #c1d9ff;
+}
+
+.dialog-header {
+ background: #e0edfe;
+ padding: 10px;
+ font-weight: bold;
+ font-size: 1.6em;
+ color: #000;
+ cursor: move;
+}
+
+.dialog-body {
+ overflow: auto;
+ font-size: 1.3em;
+ padding: 15px;
+}
+
+.dialog-instruction {
+ padding: 0 0 7px;
+}
+
+.dialog-footer {
+ font-size: 1.3em;
+ background: #eee;
+ padding: 10px;
+}
+
+.dialog-busy {
+ width: 400px;
+ border: none;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ border-radius: 5px;
+}
+
+/**********************************************************
+ * Transform Dialog
+ *********************************************************/
+
+#expression-preview-tabs .ui-tabs-nav li a {
+ padding: 0.15em 1em;
+}
+
+textarea.expression-preview-code {
+ font-family: monospace;
+ height: 5em;
+ vertical-align: top;
+}
+
+.expression-preview-parsing-status {
+ color: #999;
+}
+
+.expression-preview-parsing-status.error {
+ color: red;
+}
+
+#expression-preview-tabs-preview,
+#expression-preview-tabs-help,
+#expression-preview-tabs-history,
+#expression-preview-tabs-starred {
+ padding: 5px;
+ overflow: hidden;
+}
+
+#expression-preview-tabs-preview > div,
+#expression-preview-tabs-help > div,
+#expression-preview-tabs-history > div,
+#expression-preview-tabs-starred {
+ height: 200px;
+ overflow: auto;
+}
+
+#expression-preview-tabs-preview td, #expression-preview-tabs-preview th,
+#expression-preview-tabs-help td, #expression-preview-tabs-help th,
+#expression-preview-tabs-history td, #expression-preview-tabs-history th,
+#expression-preview-tabs-starred td, #expression-preview-tabs-starred th {
+ padding: 5px;
+}
+
+.expression-preview-table-wrapper {
+ padding: 7px;
+}
+
+.expression-preview-container td {
+ padding: 2px 5px;
+ border-top: 1px solid #ccc;
+}
+
+td.expression-preview-heading {
+ border-top: none;
+ background: #ddd;
+ font-weight: bold;
+}
+
+td.expression-preview-value {
+ max-width: 250px !important;
+ overflow-x: hidden;
+}
+
+.expression-preview-special-value {
+ color: #aaa;
+}
+
+.expression-preview-help-container h3 {
+ margin-top: 15px;
+ margin-bottom: 7px;
+ border-bottom: 1px solid #999;
+}
+
+.expression-preview-doc-item-title {
+ font-weight: bold;
+ text-align: right;
+}
+
+.expression-preview-doc-item-params {
+}
+
+.expression-preview-doc-item-returns {
+}
+
+.expression-preview-doc-item-desc {
+ color: #666;
+}
+
+
+/**********************************************************
+ * Read-only mode
+ *********************************************************/
+
+.recline-read-only .no-hidden .recline-grid tr td:first-child,
+.recline-read-only .no-hidden .recline-grid tr th:first-child
+{
+ display: none;
+}
+
+.recline-read-only .recline-grid .write-op,
+.recline-read-only .recline-grid a.data-table-cell-edit
+{
+ display: none;
+}
+
+.recline-read-only a.row-header-menu {
+ display: none;
+}
+
diff --git a/ckan/public/scripts/vendor/recline/css/map.css b/ckan/public/scripts/vendor/recline/css/map.css
index c8adde7..f1f2da2 100644
--- a/ckan/public/scripts/vendor/recline/css/map.css
+++ b/ckan/public/scripts/vendor/recline/css/map.css
@@ -1,4 +1,4 @@
-.data-map-container .map {
+.recline-map .map {
height: 500px;
}
@@ -6,18 +6,23 @@
* Editor
*********************************************************/
-.data-map-container .editor {
+.recline-map .editor {
float: right;
width: 200px;
padding-left: 0px;
margin-left: 10px;
}
-.data-map-container .editor form {
+.recline-map .editor form {
padding-left: 4px;
}
-.data-map-container .editor select {
- width: 100%;
+.recline-map .editor select {
+ width: 100%;
}
+.recline-map .editor .editor-options {
+ margin-top: 10px;
+ border-top: 1px solid gray;
+ padding: 5px 0;
+}
diff --git a/ckan/public/scripts/vendor/recline/recline.js b/ckan/public/scripts/vendor/recline/recline.js
index 2871c6d..c1f9185 100644
--- a/ckan/public/scripts/vendor/recline/recline.js
+++ b/ckan/public/scripts/vendor/recline/recline.js
@@ -86,7 +86,7 @@ this.recline.Model = this.recline.Model || {};
//
// @property {number} docCount: total number of documents in this dataset
//
-// @property {Backend} backend: the Backend (instance) for this Dataset
+// @property {Backend} backend: the Backend (instance) for this Dataset.
//
// @property {Query} queryState: `Query` object which stores current
// queryState. queryState may be edited by other components (e.g. a query
@@ -96,14 +96,24 @@ this.recline.Model = this.recline.Model || {};
// Facets.
my.Dataset = Backbone.Model.extend({
__type__: 'Dataset',
+
// ### initialize
//
// Sets up instance properties (see above)
+ //
+ // @param {Object} model: standard set of model attributes passed to Backbone models
+ //
+ // @param {Object or String} backend: Backend instance (see
+ // `recline.Backend.Base`) or a string specifying that instance. The
+ // string specifying may be a full class path e.g.
+ // 'recline.Backend.ElasticSearch' or a simple name e.g.
+ // 'elasticsearch' or 'ElasticSearch' (in this case must be a Backend in
+ // recline.Backend module)
initialize: function(model, backend) {
_.bindAll(this, 'query');
this.backend = backend;
- if (backend && backend.constructor == String) {
- this.backend = my.backends[backend];
+ if (typeof(backend) === 'string') {
+ this.backend = this._backendFromString(backend);
}
this.fields = new my.FieldList();
this.currentDocuments = new my.DocumentList();
@@ -167,9 +177,73 @@ my.Dataset = Backbone.Model.extend({
data.docCount = this.docCount;
data.fields = this.fields.toJSON();
return data;
+ },
+
+ // ### _backendFromString(backendString)
+ //
+ // See backend argument to initialize for details
+ _backendFromString: function(backendString) {
+ var parts = backendString.split('.');
+ // walk through the specified path xxx.yyy.zzz to get the final object which should be backend class
+ var current = window;
+ for(ii=0;ii<parts.length;ii++) {
+ if (!current) {
+ break;
+ }
+ current = current[parts[ii]];
+ }
+ if (current) {
+ return new current();
+ }
+
+ // alternatively we just had a simple string
+ var backend = null;
+ if (recline && recline.Backend) {
+ _.each(_.keys(recline.Backend), function(name) {
+ if (name.toLowerCase() === backendString.toLowerCase()) {
+ backend = new recline.Backend[name]();
+ }
+ });
+ }
+ return backend;
}
});
+
+// ### Dataset.restore
+//
+// Restore a Dataset instance from a serialized state. Serialized state for a
+// Dataset is an Object like:
+//
+// <pre>
+// {
+// backend: {backend type - i.e. value of dataset.backend.__type__}
+// dataset: {dataset info needed for loading -- result of dataset.toJSON() would be sufficient but can be simpler }
+// // convenience - if url provided and dataste not this be used as dataset url
+// url: {dataset url}
+// ...
+// }
+my.Dataset.restore = function(state) {
+ // hack-y - restoring a memory dataset does not mean much ...
+ var dataset = null;
+ if (state.url && !state.dataset) {
+ state.dataset = {url: state.url};
+ }
+ if (state.backend === 'memory') {
+ dataset = recline.Backend.createDataset(
+ [{stub: 'this is a stub dataset because we do not restore memory datasets'}],
+ [],
+ state.dataset // metadata
+ );
+ } else {
+ dataset = new recline.Model.Dataset(
+ state.dataset,
+ state.backend
+ );
+ }
+ return dataset;
+};
+
// ## <a id="document">A Document (aka Row)</a>
//
// A single entry or row in the dataset
@@ -211,7 +285,8 @@ my.DocumentList = Backbone.Collection.extend({
// * format: (optional) used to indicate how the data should be formatted. For example:
// * type=date, format=yyyy-mm-dd
// * type=float, format=percentage
-// * type=float, format='###,###.##'
+// * type=string, format=link (render as hyperlink)
+// * type=string, format=markdown (render as markdown if Showdown available)
// * is_derived: (default: false) attribute indicating this field has no backend data but is just derived from other fields (see below).
//
// Following additional instance properties:
@@ -267,6 +342,22 @@ my.Field = Backbone.Model.extend({
if (format === 'percentage') {
return val + '%';
}
+ return val;
+ },
+ 'string': function(val, field, doc) {
+ var format = field.get('format');
+ if (format === 'link') {
+ return '<a href="VAL">VAL</a>'.replace(/VAL/g, val);
+ } else if (format === 'markdown') {
+ if (typeof Showdown !== 'undefined') {
+ var showdown = new Showdown.converter();
+ out = showdown.makeHtml(val);
+ return out;
+ } else {
+ return val;
+ }
+ }
+ return val;
}
}
});
@@ -449,6 +540,13 @@ my.FacetList = Backbone.Collection.extend({
model: my.Facet
});
+// ## Object State
+//
+// Convenience Backbone model for storing (configuration) state of objects like Views.
+my.ObjectState = Backbone.Model.extend({
+});
+
+
// ## Backend registry
//
// Backends will register themselves by id into this registry
@@ -618,10 +716,10 @@ this.recline.View = this.recline.View || {};
// ## Graph view for a Dataset using Flot graphing library.
//
-// Initialization arguments:
+// Initialization arguments (in a hash in first parameter):
//
// * model: recline.Model.Dataset
-// * config: (optional) graph configuration hash of form:
+// * state: (optional) configuration hash of form:
//
// {
// group: {column name for x-axis},
@@ -631,10 +729,10 @@ this.recline.View = this.recline.View || {};
//
// NB: should *not* provide an el argument to the view but must let the view
// generate the element itself (you can then append view.el to the DOM.
-my.FlotGraph = Backbone.View.extend({
+my.Graph = Backbone.View.extend({
tagName: "div",
- className: "data-graph-container",
+ className: "recline-graph",
template: ' \
<div class="editor"> \
@@ -697,7 +795,7 @@ my.FlotGraph = Backbone.View.extend({
'click .action-toggle-help': 'toggleHelp'
},
- initialize: function(options, config) {
+ initialize: function(options) {
var self = this;
this.el = $(this.el);
_.bindAll(this, 'render', 'redraw');
@@ -707,18 +805,14 @@ my.FlotGraph = Backbone.View.extend({
this.model.fields.bind('add', this.render);
this.model.currentDocuments.bind('add', this.redraw);
this.model.currentDocuments.bind('reset', this.redraw);
- var configFromHash = my.parseHashQueryString().graph;
- if (configFromHash) {
- configFromHash = JSON.parse(configFromHash);
- }
- this.chartConfig = _.extend({
+ var stateData = _.extend({
group: null,
series: [],
graphType: 'lines-and-points'
},
- configFromHash,
- config
- );
+ options.state
+ );
+ this.state = new recline.Model.ObjectState(stateData);
this.render();
},
@@ -740,13 +834,12 @@ my.FlotGraph = Backbone.View.extend({
var series = this.$series.map(function () {
return $(this).val();
});
- this.chartConfig.series = $.makeArray(series);
- this.chartConfig.group = this.el.find('.editor-group select').val();
- this.chartConfig.graphType = this.el.find('.editor-type select').val();
- // update navigation
- var qs = my.parseHashQueryString();
- qs.graph = JSON.stringify(this.chartConfig);
- my.setHashQueryString(qs);
+ var updatedState = {
+ series: $.makeArray(series),
+ group: this.el.find('.editor-group select').val(),
+ graphType: this.el.find('.editor-type select').val()
+ };
+ this.state.set(updatedState);
this.redraw();
},
@@ -762,7 +855,7 @@ my.FlotGraph = Backbone.View.extend({
return;
}
var series = this.createSeries();
- var options = this.getGraphOptions(this.chartConfig.graphType);
+ var options = this.getGraphOptions(this.state.attributes.graphType);
this.plot = $.plot(this.$graph, series, options);
this.setupTooltips();
// create this.plot and cache it
@@ -783,7 +876,7 @@ my.FlotGraph = Backbone.View.extend({
// special tickformatter to show labels rather than numbers
var tickFormatter = function (val) {
if (self.model.currentDocuments.models[val]) {
- var out = self.model.currentDocuments.models[val].get(self.chartConfig.group);
+ var out = self.model.currentDocuments.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;
@@ -866,14 +959,14 @@ my.FlotGraph = Backbone.View.extend({
var y = item.datapoint[1];
// 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.chartConfig.group);
+ x = self.model.currentDocuments.models[x].get(self.state.attributes.group);
} else {
x = x.toFixed(2);
}
y = y.toFixed(2);
var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
- group: self.chartConfig.group,
+ group: self.state.attributes.group,
x: x,
series: item.series.label,
y: y
@@ -891,25 +984,23 @@ my.FlotGraph = Backbone.View.extend({
createSeries: function () {
var self = this;
var series = [];
- if (this.chartConfig) {
- $.each(this.chartConfig.series, function (seriesIndex, field) {
- var points = [];
- $.each(self.model.currentDocuments.models, function (index, doc) {
- var x = doc.get(self.chartConfig.group);
- var y = doc.get(field);
- if (typeof x === 'string') {
- x = index;
- }
- // horizontal bar chart
- if (self.chartConfig.graphType == 'bars') {
- points.push([y, x]);
- } else {
- points.push([x, y]);
- }
- });
- series.push({data: points, label: field});
+ _.each(this.state.attributes.series, function(field) {
+ var points = [];
+ _.each(self.model.currentDocuments.models, function(doc, index) {
+ var x = doc.get(self.state.attributes.group);
+ var y = doc.get(field);
+ if (typeof x === 'string') {
+ x = index;
+ }
+ // horizontal bar chart
+ if (self.state.attributes.graphType == 'bars') {
+ points.push([y, x]);
+ } else {
+ points.push([x, y]);
+ }
});
- }
+ series.push({data: points, label: field});
+ });
return series;
},
@@ -969,12 +1060,12 @@ this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
-// ## DataGrid
+// ## (Data) Grid Dataset View
//
// Provides a tabular view on a Dataset.
//
// Initialize it with a `recline.Model.Dataset`.
-my.DataGrid = Backbone.View.extend({
+my.Grid = Backbone.View.extend({
tagName: "div",
className: "recline-grid-container",
@@ -985,12 +1076,16 @@ my.DataGrid = Backbone.View.extend({
this.model.currentDocuments.bind('add', this.render);
this.model.currentDocuments.bind('reset', this.render);
this.model.currentDocuments.bind('remove', this.render);
- this.state = {};
- this.hiddenFields = [];
+ this.tempState = {};
+ var state = _.extend({
+ hiddenFields: []
+ }, modelEtc.state
+ );
+ this.state = new recline.Model.ObjectState(state);
},
events: {
- 'click .column-header-menu': 'onColumnHeaderClick',
+ 'click .column-header-menu .data-table-menu li a': 'onColumnHeaderClick',
'click .row-header-menu': 'onRowHeaderClick',
'click .root-header-menu': 'onRootHeaderClick',
'click .data-table-menu li a': 'onMenuClick'
@@ -1012,11 +1107,11 @@ my.DataGrid = Backbone.View.extend({
// Column and row menus
onColumnHeaderClick: function(e) {
- this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field');
+ this.tempState.currentColumn = $(e.target).closest('.column-header').attr('data-field');
},
onRowHeaderClick: function(e) {
- this.state.currentRow = $(e.target).parents('tr:first').attr('data-id');
+ this.tempState.currentRow = $(e.target).parents('tr:first').attr('data-id');
},
onRootHeaderClick: function(e) {
@@ -1024,7 +1119,7 @@ my.DataGrid = Backbone.View.extend({
{{#columns}} \
<li><a data-action="showColumn" data-column="{{.}}" href="JavaScript:void(0);">Show column: {{.}}</a></li> \
{{/columns}}';
- var tmp = $.mustache(tmpl, {'columns': this.hiddenFields});
+ var tmp = $.mustache(tmpl, {'columns': this.state.get('hiddenFields')});
this.el.find('.root-header-menu .dropdown-menu').html(tmp);
},
@@ -1032,15 +1127,15 @@ my.DataGrid = Backbone.View.extend({
var self = this;
e.preventDefault();
var actions = {
- bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}); },
+ bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.tempState.currentColumn}); },
facet: function() {
- self.model.queryState.addFacet(self.state.currentColumn);
+ self.model.queryState.addFacet(self.tempState.currentColumn);
},
facet_histogram: function() {
- self.model.queryState.addHistogramFacet(self.state.currentColumn);
+ self.model.queryState.addHistogramFacet(self.tempState.currentColumn);
},
filter: function() {
- self.model.queryState.addTermFilter(self.state.currentColumn, '');
+ self.model.queryState.addTermFilter(self.tempState.currentColumn, '');
},
transform: function() { self.showTransformDialog('transform'); },
sortAsc: function() { self.setColumnSort('asc'); },
@@ -1051,7 +1146,7 @@ my.DataGrid = Backbone.View.extend({
var doc = _.find(self.model.currentDocuments.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.state.currentRow;
+ return doc.id == self.tempState.currentRow;
});
doc.destroy().then(function() {
self.model.currentDocuments.remove(doc);
@@ -1070,7 +1165,7 @@ my.DataGrid = Backbone.View.extend({
var view = new my.ColumnTransform({
model: this.model
});
- view.state = this.state;
+ view.state = this.tempState;
view.render();
$el.empty();
$el.append(view.el);
@@ -1096,17 +1191,20 @@ my.DataGrid = Backbone.View.extend({
setColumnSort: function(order) {
var sort = [{}];
- sort[0][this.state.currentColumn] = {order: order};
+ sort[0][this.tempState.currentColumn] = {order: order};
this.model.query({sort: sort});
},
hideColumn: function() {
- this.hiddenFields.push(this.state.currentColumn);
+ var hiddenFields = this.state.get('hiddenFields');
+ hiddenFields.push(this.tempState.currentColumn);
+ this.state.set({hiddenFields: hiddenFields});
this.render();
},
showColumn: function(e) {
- this.hiddenFields = _.without(this.hiddenFields, $(e.target).data('column'));
+ var hiddenFields = _.without(this.state.get('hiddenFields'), $(e.target).data('column'));
+ this.state.set({hiddenFields: hiddenFields});
this.render();
},
@@ -1162,41 +1260,41 @@ my.DataGrid = Backbone.View.extend({
render: function() {
var self = this;
this.fields = this.model.fields.filter(function(field) {
- return _.indexOf(self.hiddenFields, field.id) == -1;
+ return _.indexOf(self.state.get('hiddenFields'), field.id) == -1;
});
var htmls = $.mustache(this.template, this.toTemplateJSON());
this.el.html(htmls);
this.model.currentDocuments.forEach(function(doc) {
var tr = $('<tr />');
self.el.find('tbody').append(tr);
- var newView = new my.DataGridRow({
+ var newView = new my.GridRow({
model: doc,
el: tr,
fields: self.fields
});
newView.render();
});
- this.el.toggleClass('no-hidden', (self.hiddenFields.length === 0));
+ this.el.toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0));
return this;
}
});
-// ## DataGridRow View for rendering an individual document.
+// ## GridRow View for rendering an individual document.
//
// Since we want this to update in place it is up to creator to provider the element to attach to.
//
-// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the DataGrid.
+// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the Grid.
//
// Example:
//
// <pre>
-// var row = new DataGridRow({
+// var row = new GridRow({
// model: dataset-document,
// el: dom-element,
// fields: mydatasets.fields // a FieldList object
// });
// </pre>
-my.DataGridRow = Backbone.View.extend({
+my.GridRow = Backbone.View.extend({
initialize: function(initData) {
_.bindAll(this, 'render');
this._fields = initData.fields;
@@ -1301,21 +1399,21 @@ this.recline.View = this.recline.View || {};
// [GeoJSON](http://geojson.org) objects or two fields with latitude and
// longitude coordinates.
//
-// Initialization arguments:
-//
-// * options: initial options. They must contain a model:
-//
-// {
-// model: {recline.Model.Dataset}
-// }
-//
-// * config: (optional) map configuration hash (not yet used)
-//
+// Initialization arguments are as standard for Dataset Views. State object may
+// have the following (optional) configuration options:
//
+// <pre>
+// {
+// // geomField if specified will be used in preference to lat/lon
+// geomField: {id of field containing geometry in the dataset}
+// lonField: {id of field containing longitude in the dataset}
+// latField: {id of field containing latitude in the dataset}
+// }
+// </pre>
my.Map = Backbone.View.extend({
tagName: 'div',
- className: 'data-map-container',
+ className: 'recline-map',
template: ' \
<div class="editor"> \
@@ -1364,6 +1462,11 @@ my.Map = Backbone.View.extend({
<div class="editor-buttons"> \
<button class="btn editor-update-map">Update</button> \
</div> \
+ <div class="editor-options" > \
+ <label class="checkbox"> \
+ <input type="checkbox" id="editor-auto-zoom" checked="checked" /> \
+ Auto zoom to features</label> \
+ </div> \
<input type="hidden" class="editor-id" value="map-1" /> \
</div> \
</form> \
@@ -1381,17 +1484,16 @@ my.Map = Backbone.View.extend({
// Define here events for UI elements
events: {
'click .editor-update-map': 'onEditorSubmit',
- 'change .editor-field-type': 'onFieldTypeChange'
+ 'change .editor-field-type': 'onFieldTypeChange',
+ 'change #editor-auto-zoom': 'onAutoZoomChange'
},
-
- initialize: function(options, config) {
+ initialize: function(options) {
var self = this;
-
this.el = $(this.el);
// Listen to changes in the fields
- this.model.bind('change', function() {
+ this.model.fields.bind('change', function() {
self._setupGeometryField();
});
this.model.fields.bind('add', this.render);
@@ -1402,17 +1504,40 @@ my.Map = Backbone.View.extend({
// Listen to changes in the documents
this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
+ this.model.currentDocuments.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')});
- // If the div was hidden, Leaflet needs to recalculate some sizes
- // to display properly
this.bind('view:show',function(){
+ // If the div was hidden, Leaflet needs to recalculate some sizes
+ // to display properly
+ if (self.map){
self.map.invalidateSize();
+ if (self._zoomPending && self.autoZoom) {
+ self._zoomToFeatures();
+ self._zoomPending = false;
+ }
+ }
+ self.visible = true;
+ });
+ this.bind('view:hide',function(){
+ self.visible = false;
});
- this.mapReady = false;
+ var stateData = _.extend({
+ geomField: null,
+ lonField: null,
+ latField: null
+ },
+ options.state
+ );
+ this.state = new recline.Model.ObjectState(stateData);
+ this.autoZoom = true;
+ this.mapReady = false;
this.render();
},
@@ -1429,12 +1554,12 @@ my.Map = Backbone.View.extend({
this.$map = this.el.find('.panel.map');
if (this.geomReady && this.model.fields.length){
- if (this._geomFieldName){
- this._selectOption('editor-geom-field',this._geomFieldName);
+ if (this.state.get('geomField')){
+ this._selectOption('editor-geom-field',this.state.get('geomField'));
$('#editor-field-type-geom').attr('checked','checked').change();
} else{
- this._selectOption('editor-lon-field',this._lonFieldName);
- this._selectOption('editor-lat-field',this._latFieldName);
+ this._selectOption('editor-lon-field',this.state.get('lonField'));
+ this._selectOption('editor-lat-field',this.state.get('latField'));
$('#editor-field-type-latlon').attr('checked','checked').change();
}
}
@@ -1463,9 +1588,7 @@ my.Map = Backbone.View.extend({
// * refresh: Clear existing features and add all current documents
//
redraw: function(action,doc){
-
var self = this;
-
action = action || 'refresh';
if (this.geomReady && this.mapReady){
@@ -1479,6 +1602,13 @@ my.Map = Backbone.View.extend({
this.features.clearLayers();
this._add(this.model.currentDocuments.models);
}
+ if (action != 'reset' && this.autoZoom){
+ if (this.visible){
+ this._zoomToFeatures();
+ } else {
+ this._zoomPending = true;
+ }
+ }
}
},
@@ -1494,14 +1624,19 @@ my.Map = Backbone.View.extend({
onEditorSubmit: function(e){
e.preventDefault();
if ($('#editor-field-type-geom').attr('checked')){
- this._geomFieldName = $('.editor-geom-field > select > option:selected').val();
- this._latFieldName = this._lonFieldName = false;
+ this.state.set({
+ geomField: $('.editor-geom-field > select > option:selected').val(),
+ lonField: null,
+ latField: null
+ });
} else {
- this._geomFieldName = false;
- this._latFieldName = $('.editor-lat-field > select > option:selected').val();
- this._lonFieldName = $('.editor-lon-field > select > option:selected').val();
+ this.state.set({
+ geomField: null,
+ lonField: $('.editor-lon-field > select > option:selected').val(),
+ latField: $('.editor-lat-field > select > option:selected').val()
+ });
}
- this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+ this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
this.redraw();
return false;
@@ -1520,6 +1655,10 @@ my.Map = Backbone.View.extend({
}
},
+ onAutoZoomChange: function(e){
+ this.autoZoom = !this.autoZoom;
+ },
+
// Private: Add one or n features to the map
//
// For each document passed, a GeoJSON geometry will be extracted and added
@@ -1534,9 +1673,12 @@ my.Map = Backbone.View.extend({
if (!(docs instanceof Array)) docs = [docs];
+ var count = 0;
+ var wrongSoFar = 0;
_.every(docs,function(doc){
+ count += 1;
var feature = self._getGeometryFromDocument(doc);
- if (typeof feature === 'undefined'){
+ if (typeof feature === 'undefined' || feature === null){
// Empty field
return true;
} else if (feature instanceof Object){
@@ -1544,7 +1686,9 @@ my.Map = Backbone.View.extend({
// TODO: mustache?
html = ''
for (key in doc.attributes){
- html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>'
+ if (!(self.state.get('geomField') && key == self.state.get('geomField'))){
+ html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>';
+ }
}
feature.properties = {popupContent: html};
@@ -1553,16 +1697,20 @@ my.Map = Backbone.View.extend({
feature.properties.cid = doc.cid;
try {
- self.features.addGeoJSON(feature);
+ self.features.addGeoJSON(feature);
} catch (except) {
- var msg = 'Wrong geometry value';
- if (except.message) msg += ' (' + except.message + ')';
+ wrongSoFar += 1;
+ var msg = 'Wrong geometry value';
+ if (except.message) msg += ' (' + except.message + ')';
+ if (wrongSoFar <= 10) {
my.notify(msg,{category:'error'});
- return false;
+ }
}
} else {
- my.notify('Wrong geometry value',{category:'error'});
- return false;
+ wrongSoFar += 1
+ if (wrongSoFar <= 10) {
+ my.notify('Wrong geometry value',{category:'error'});
+ }
}
return true;
});
@@ -1576,7 +1724,7 @@ my.Map = Backbone.View.extend({
if (!(docs instanceof Array)) docs = [docs];
- _.each(doc,function(doc){
+ _.each(docs,function(doc){
for (key in self.features._layers){
if (self.features._layers[key].cid == doc.cid){
self.features.removeLayer(self.features._layers[key]);
@@ -1590,18 +1738,25 @@ my.Map = Backbone.View.extend({
//
_getGeometryFromDocument: function(doc){
if (this.geomReady){
- if (this._geomFieldName){
- // We assume that the contents of the field are a valid GeoJSON object
- return doc.attributes[this._geomFieldName];
- } else if (this._lonFieldName && this._latFieldName){
+ if (this.state.get('geomField')){
+ var value = doc.get(this.state.get('geomField'));
+ if (typeof(value) === 'string'){
+ // We have a GeoJSON string representation
+ return $.parseJSON(value);
+ } else {
+ // We assume that the 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
- return {
- type: 'Point',
- coordinates: [
- doc.attributes[this._lonFieldName],
- doc.attributes[this._latFieldName]
- ]
- };
+ var lon = doc.get(this.state.get('lonField'));
+ var lat = doc.get(this.state.get('latField'));
+ if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
+ return {
+ type: 'Point',
+ coordinates: [lon,lat]
+ };
+ }
}
return null;
}
@@ -1613,12 +1768,16 @@ my.Map = Backbone.View.extend({
// If not found, the user can define them via the UI form.
_setupGeometryField: function(){
var geomField, latField, lonField;
-
- this._geomFieldName = this._checkField(this.geometryFieldNames);
- this._latFieldName = this._checkField(this.latitudeFieldNames);
- this._lonFieldName = this._checkField(this.longitudeFieldNames);
-
- this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+ this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
+ // should not overwrite if we have already set this (e.g. explicitly via state)
+ if (!this.geomReady) {
+ this.state.set({
+ geomField: this._checkField(this.geometryFieldNames),
+ latField: this._checkField(this.latitudeFieldNames),
+ lonField: this._checkField(this.longitudeFieldNames)
+ });
+ this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
+ }
},
// Private: Check if a field in the current model exists in the provided
@@ -1637,6 +1796,18 @@ my.Map = Backbone.View.extend({
return null;
},
+ // Private: Zoom to map to current features extent if any, or to the full
+ // extent if none.
+ //
+ _zoomToFeatures: function(){
+ var bounds = this.features.getBounds();
+ if (bounds){
+ this.map.fitBounds(bounds);
+ } else {
+ this.map.setView(new L.LatLng(0, 0), 2);
+ }
+ },
+
// Private: Sets up the Leaflet map control and the features layer.
//
// The map uses a base layer from [MapQuest](http://www.mapquest.com) based
@@ -1661,6 +1832,24 @@ my.Map = Backbone.View.extend({
}
});
+
+ // This will be available in the next Leaflet stable release.
+ // In the meantime we add it manually to our layer.
+ this.features.getBounds = function(){
+ var bounds = new L.LatLngBounds();
+ this._iterateLayers(function (layer) {
+ if (layer instanceof L.Marker){
+ bounds.extend(layer.getLatLng());
+ } else {
+ if (layer.getBounds){
+ bounds.extend(layer.getBounds().getNorthEast());
+ bounds.extend(layer.getBounds().getSouthWest());
+ }
+ }
+ }, this);
+ return (typeof bounds.getNorthEast() !== 'undefined') ? bounds : null;
+ }
+
this.map.addLayer(this.features);
this.map.setView(new L.LatLng(0, 0), 2);
@@ -1895,6 +2084,85 @@ my.ColumnTransform = Backbone.View.extend({
})(jQuery, recline.View);
/*jshint multistr:true */
+
+// # Recline Views
+//
+// Recline Views are Backbone Views and in keeping with normal Backbone views
+// are Widgets / Components displaying something in the DOM. Like all Backbone
+// views they have a pointer to a model or a collection and is bound to an
+// element.
+//
+// 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`.
+//
+// ### Writing your own Views
+//
+// See the existing Views.
+//
+// ----
+
+// Standard JS module setup
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -1907,47 +2175,62 @@ this.recline.View = this.recline.View || {};
// var myExplorer = new model.recline.DataExplorer({
// model: {{recline.Model.Dataset instance}}
// el: {{an existing dom element}}
-// views: {{page views}}
-// config: {{config options -- see below}}
+// views: {{dataset views}}
+// state: {{state configuration -- see below}}
// });
// </pre>
//
// ### Parameters
//
-// **model**: (required) Dataset instance.
+// **model**: (required) recline.model.Dataset instance.
//
-// **el**: (required) DOM element.
+// **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 views (Grid, Graph etc) for DataExplorer to
-// show. This is an array of view hashes. If not provided
-// just initialize a DataGrid with id 'grid'. Example:
+// **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.DataGrid({
+// view: new recline.View.Grid({
// model: dataset
// })
// },
// {
// id: 'graph',
// label: 'Graph',
-// view: new recline.View.FlotGraph({
+// view: new recline.View.Graph({
// model: dataset
// })
// }
// ];
// </pre>
//
-// **config**: Config options like:
+// **state**: standard state config for this view. This state is slightly
+// special as it includes config of many of the subviews.
//
-// * readOnly: true/false (default: false) value indicating whether to
-// operate in read-only mode (hiding all editing options).
+// <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>
//
-// NB: the element already being in the DOM is important for rendering of
-// FlotGraph subview.
+// 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"> \
@@ -1956,7 +2239,7 @@ my.DataExplorer = Backbone.View.extend({
<div class="header"> \
<ul class="navigation"> \
{{#views}} \
- <li><a href="#{{id}}" class="btn">{{label}}</a> \
+ <li><a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
{{/views}} \
</ul> \
<div class="recline-results-info"> \
@@ -1979,19 +2262,14 @@ my.DataExplorer = Backbone.View.extend({
</div> \
',
events: {
- 'click .menu-right a': 'onMenuClick'
+ 'click .menu-right a': '_onMenuClick',
+ 'click .navigation a': '_onSwitchView'
},
initialize: function(options) {
var self = this;
this.el = $(this.el);
- this.config = _.extend({
- readOnly: false
- },
- options.config);
- if (this.config.readOnly) {
- this.setReadOnly();
- }
+ 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;
@@ -1999,13 +2277,38 @@ my.DataExplorer = Backbone.View.extend({
this.pageViews = [{
id: 'grid',
label: 'Grid',
- view: new my.DataGrid({
- model: this.model
- })
+ 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')
+ }),
}];
}
- // this must be called after pageViews are created
+ // these must be called after pageViews are created
this.render();
+ this._bindStateChanges();
+ // 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.router = new Backbone.Router();
this.setupRouting();
@@ -2021,7 +2324,7 @@ my.DataExplorer = Backbone.View.extend({
var qs = my.parseHashQueryString();
qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON());
var out = my.getNewHashForQueryString(qs);
- self.router.navigate(out);
+ // self.router.navigate(out);
});
this.model.bind('query:fail', function(error) {
my.clearNotifications();
@@ -2045,11 +2348,7 @@ my.DataExplorer = Backbone.View.extend({
// note this.model and dataset returned are the same
this.model.fetch()
.done(function(dataset) {
- var queryState = my.parseHashQueryString().reclineQuery;
- if (queryState) {
- queryState = JSON.parse(queryState);
- }
- self.model.query(queryState);
+ self.model.query(self.state.get('query'));
})
.fail(function(error) {
my.notify(error.message, {category: 'error', persist: true});
@@ -2057,12 +2356,11 @@ my.DataExplorer = Backbone.View.extend({
},
setReadOnly: function() {
- this.el.addClass('read-only');
+ this.el.addClass('recline-read-only');
},
render: function() {
var tmplData = this.model.toTemplateJSON();
- tmplData.displayCount = this.config.displayCount;
tmplData.views = this.pageViews;
var template = $.mustache(this.template, tmplData);
$(this.el).html(template);
@@ -2089,20 +2387,22 @@ my.DataExplorer = Backbone.View.extend({
setupRouting: function() {
var self = this;
// Default route
- this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
- self.updateNav(self.pageViews[0].id, queryString);
- });
- $.each(this.pageViews, function(idx, view) {
- self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) {
- self.updateNav(viewId, queryString);
- });
+// this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
+// self.updateNav(self.pageViews[0].id, queryString);
+// });
+// $.each(this.pageViews, function(idx, view) {
+// self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) {
+// self.updateNav(viewId, queryString);
+// });
+// });
+ this.router.route(/.*/, 'view', function() {
});
},
- updateNav: function(pageName, queryString) {
+ 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[href=#' + pageName + ']');
+ var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]');
$el.parent().addClass('active');
$el.addClass('disabled');
// show the specific page
@@ -2117,7 +2417,7 @@ my.DataExplorer = Backbone.View.extend({
});
},
- onMenuClick: function(e) {
+ _onMenuClick: function(e) {
e.preventDefault();
var action = $(e.target).attr('data-action');
if (action === 'filters') {
@@ -2125,9 +2425,76 @@ my.DataExplorer = Backbone.View.extend({
} 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__,
+ dataset: this.model.toJSON(),
+ 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();
+ self.state.set(update);
+ });
+ }
+ });
}
});
+// ### 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: ' \
@@ -2403,6 +2770,9 @@ my.composeQueryString = function(queryParams) {
var queryString = '?';
var items = [];
$.each(queryParams, function(key, value) {
+ if (typeof(value) === 'object') {
+ value = JSON.stringify(value);
+ }
items.push(key + '=' + value);
});
queryString += items.join('&');
@@ -2484,10 +2854,27 @@ this.recline.Backend = this.recline.Backend || {};
// ## recline.Backend.Base
//
// Base class for backends providing a template and convenience functions.
- // You do not have to inherit from this class but even when not it does provide guidance on the functions you must implement.
+ // You do not have to inherit from this class but even when not it does
+ // provide guidance on the functions you must implement.
//
// Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.
my.Base = Backbone.Model.extend({
+ // ### __type__
+ //
+ // 'type' of this backend. This should be either the class path for this
+ // object as a string (e.g. recline.Backend.Memory) or for Backends within
+ // recline.Backend module it may be their class name.
+ //
+ // This value is used as an identifier for this backend when initializing
+ // backends (see recline.Model.Dataset.initialize).
+ __type__: 'base',
+
+
+ // ### readonly
+ //
+ // Class level attribute indicating that this backend is read-only (that
+ // is, cannot be written to).
+ readonly: true,
// ### sync
//
@@ -2549,6 +2936,32 @@ this.recline.Backend = this.recline.Backend || {};
query: function(model, queryObj) {
},
+ // ### _makeRequest
+ //
+ // Just $.ajax but in any headers in the 'headers' attribute of this
+ // Backend instance. Example:
+ //
+ // <pre>
+ // var jqxhr = this._makeRequest({
+ // url: the-url
+ // });
+ // </pre>
+ _makeRequest: function(data) {
+ var headers = this.get('headers');
+ var extras = {};
+ if (headers) {
+ extras = {
+ beforeSend: function(req) {
+ _.each(headers, function(value, key) {
+ req.setRequestHeader(key, value);
+ });
+ }
+ };
+ }
+ var data = _.extend(extras, data);
+ return $.ajax(data);
+ },
+
// convenience method to convert simple set of documents / rows to a QueryResult
_docsToQueryResult: function(rows) {
var hits = _.map(rows, function(row) {
@@ -2607,6 +3020,8 @@ this.recline.Backend = this.recline.Backend || {};
//
// Note that this is a **read-only** backend.
my.DataProxy = my.Base.extend({
+ __type__: 'dataproxy',
+ readonly: true,
defaults: {
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
},
@@ -2661,8 +3076,6 @@ this.recline.Backend = this.recline.Backend || {};
return dfd.promise();
}
});
- recline.Model.backends['dataproxy'] = new my.DataProxy();
-
}(jQuery, this.recline.Backend));
this.recline = this.recline || {};
@@ -2673,35 +3086,39 @@ this.recline.Backend = this.recline.Backend || {};
//
// Connecting to [ElasticSearch](http://www.elasticsearch.org/).
//
- // To use this backend ensure your Dataset has one of the following
- // attributes (first one found is used):
+ // Usage:
+ //
+ // <pre>
+ // var backend = new recline.Backend.ElasticSearch({
+ // // optional as can also be provided by Dataset/Document
+ // url: {url to ElasticSearch endpoint i.e. ES 'type/table' url - more info below}
+ // // optional
+ // headers: {dict of headers to add to each request}
+ // });
+ //
+ // @param {String} url: url for ElasticSearch type/table, e.g. for ES running
+ // on localhost:9200 with index // twitter and type tweet it would be:
+ //
+ // <pre>http://localhost:9200/twitter/tweet</pre>
+ //
+ // This url is optional since the ES endpoint url may be specified on the the
+ // dataset (and on a Document by the document having a dataset attribute) by
+ // having one of the following (see also `_getESUrl` function):
//
// <pre>
// elasticsearch_url
// webstore_url
// url
// </pre>
- //
- // This should point to the ES type url. E.G. for ES running on
- // localhost:9200 with index twitter and type tweet it would be
- //
- // <pre>http://localhost:9200/twitter/tweet</pre>
my.ElasticSearch = my.Base.extend({
- _getESUrl: function(dataset) {
- var out = dataset.get('elasticsearch_url');
- if (out) return out;
- out = dataset.get('webstore_url');
- if (out) return out;
- out = dataset.get('url');
- return out;
- },
+ __type__: 'elasticsearch',
+ readonly: false,
sync: function(method, model, options) {
var self = this;
if (method === "read") {
if (model.__type__ == 'Dataset') {
- var base = self._getESUrl(model);
- var schemaUrl = base + '/_mapping';
- var jqxhr = $.ajax({
+ var schemaUrl = self._getESUrl(model) + '/_mapping';
+ var jqxhr = this._makeRequest({
url: schemaUrl,
dataType: 'jsonp'
});
@@ -2720,11 +3137,77 @@ this.recline.Backend = this.recline.Backend || {};
dfd.reject(arguments);
});
return dfd.promise();
+ } else if (model.__type__ == 'Document') {
+ var base = this._getESUrl(model.dataset) + '/' + model.id;
+ return this._makeRequest({
+ url: base,
+ dataType: 'json'
+ });
+ }
+ } else if (method === 'update') {
+ if (model.__type__ == 'Document') {
+ return this.upsert(model.toJSON(), this._getESUrl(model.dataset));
+ }
+ } else if (method === 'delete') {
+ if (model.__type__ == 'Document') {
+ var url = this._getESUrl(model.dataset);
+ return this.delete(model.id, url);
}
- } else {
- alert('This backend currently only supports read operations');
}
},
+
+ // ### upsert
+ //
+ // create / update a document to ElasticSearch backend
+ //
+ // @param {Object} doc an object to insert to the index.
+ // @param {string} url (optional) url for ElasticSearch endpoint (if not
+ // defined called this._getESUrl()
+ upsert: function(doc, url) {
+ var data = JSON.stringify(doc);
+ url = url ? url : this._getESUrl();
+ if (doc.id) {
+ url += '/' + doc.id;
+ }
+ return this._makeRequest({
+ url: url,
+ type: 'POST',
+ data: data,
+ dataType: 'json'
+ });
+ },
+
+ // ### delete
+ //
+ // Delete a document from the ElasticSearch backend.
+ //
+ // @param {Object} id id of object to delete
+ // @param {string} url (optional) url for ElasticSearch endpoint (if not
+ // provided called this._getESUrl()
+ delete: function(id, url) {
+ url = url ? url : this._getESUrl();
+ url += '/' + id;
+ return this._makeRequest({
+ url: url,
+ type: 'DELETE',
+ dataType: 'json'
+ });
+ },
+
+ // ### _getESUrl
+ //
+ // get url to ElasticSearch endpoint (see above)
+ _getESUrl: function(dataset) {
+ if (dataset) {
+ var out = dataset.get('elasticsearch_url');
+ if (out) return out;
+ out = dataset.get('webstore_url');
+ if (out) return out;
+ out = dataset.get('url');
+ return out;
+ }
+ return this.get('url');
+ },
_normalizeQuery: function(queryObj) {
var out = queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj);
if (out.q !== undefined && out.q.trim() === '') {
@@ -2761,7 +3244,7 @@ this.recline.Backend = this.recline.Backend || {};
var queryNormalized = this._normalizeQuery(queryObj);
var data = {source: JSON.stringify(queryNormalized)};
var base = this._getESUrl(model);
- var jqxhr = $.ajax({
+ var jqxhr = this._makeRequest({
url: base + '/_search',
data: data,
dataType: 'jsonp'
@@ -2782,7 +3265,6 @@ this.recline.Backend = this.recline.Backend || {};
return dfd.promise();
}
});
- recline.Model.backends['elasticsearch'] = new my.ElasticSearch();
}(jQuery, this.recline.Backend));
@@ -2805,6 +3287,8 @@ this.recline.Backend = this.recline.Backend || {};
// );
// </pre>
my.GDoc = my.Base.extend({
+ __type__: 'gdoc',
+ readonly: true,
getUrl: function(dataset) {
var url = dataset.get('url');
if (url.indexOf('feeds/list') != -1) {
@@ -2922,7 +3406,6 @@ this.recline.Backend = this.recline.Backend || {};
return results;
}
});
- recline.Model.backends['gdocs'] = new my.GDoc();
}(jQuery, this.recline.Backend));
@@ -2930,7 +3413,9 @@ this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
(function($, my) {
- my.loadFromCSVFile = function(file, callback) {
+ my.loadFromCSVFile = function(file, callback, options) {
+ var encoding = options.encoding || 'UTF-8';
+
var metadata = {
id: file.name,
file: file
@@ -2938,17 +3423,17 @@ this.recline.Backend = this.recline.Backend || {};
var reader = new FileReader();
// TODO
reader.onload = function(e) {
- var dataset = my.csvToDataset(e.target.result);
+ var dataset = my.csvToDataset(e.target.result, options);
callback(dataset);
};
reader.onerror = function (e) {
alert('Failed to load file. Code: ' + e.target.error.code);
};
- reader.readAsText(file);
+ reader.readAsText(file, encoding);
};
- my.csvToDataset = function(csvString) {
- var out = my.parseCSV(csvString);
+ my.csvToDataset = function(csvString, options) {
+ var out = my.parseCSV(csvString, options);
fields = _.map(out[0], function(cell) {
return { id: cell, label: cell };
});
@@ -2963,128 +3448,135 @@ this.recline.Backend = this.recline.Backend || {};
return dataset;
};
- // Converts a Comma Separated Values string into an array of arrays.
- // Each line in the CSV becomes an array.
+ // Converts a Comma Separated Values string into an array of arrays.
+ // Each line in the CSV becomes an array.
//
- // Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats.
- //
- // @return The CSV parsed as an array
- // @type Array
- //
- // @param {String} s The string to convert
- // @param {Boolean} [trm=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported
+ // Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats.
//
+ // @return The CSV parsed as an array
+ // @type Array
+ //
+ // @param {String} s The string to convert
+ // @param {Object} options Options for loading CSV including
+ // @param {Boolean} [trim=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported
+ // @param {String} [separator=','] Separator for CSV file
// Heavily based on uselesscode's JS CSV parser (MIT Licensed):
// thttp://www.uselesscode.org/javascript/csv/
- my.parseCSV= function(s, trm) {
- // Get rid of any trailing \n
- s = chomp(s);
-
- var cur = '', // The character we are currently processing.
- inQuote = false,
- fieldQuoted = false,
- field = '', // Buffer for building up the current field
- row = [],
- out = [],
- i,
- processField;
-
- processField = function (field) {
- if (fieldQuoted !== true) {
- // If field is empty set to null
- if (field === '') {
- field = null;
- // If the field was not quoted and we are trimming fields, trim it
- } else if (trm === true) {
- field = trim(field);
- }
-
- // Convert unquoted numbers to their appropriate types
- if (rxIsInt.test(field)) {
- field = parseInt(field, 10);
- } else if (rxIsFloat.test(field)) {
- field = parseFloat(field, 10);
- }
- }
- return field;
- };
+ my.parseCSV= function(s, options) {
+ // Get rid of any trailing \n
+ s = chomp(s);
+
+ var options = options || {};
+ var trm = options.trim;
+ var separator = options.separator || ',';
+ var delimiter = options.delimiter || '"';
+
+
+ var cur = '', // The character we are currently processing.
+ inQuote = false,
+ fieldQuoted = false,
+ field = '', // Buffer for building up the current field
+ row = [],
+ out = [],
+ i,
+ processField;
+
+ processField = function (field) {
+ if (fieldQuoted !== true) {
+ // If field is empty set to null
+ if (field === '') {
+ field = null;
+ // If the field was not quoted and we are trimming fields, trim it
+ } else if (trm === true) {
+ field = trim(field);
+ }
- for (i = 0; i < s.length; i += 1) {
- cur = s.charAt(i);
-
- // If we are at a EOF or EOR
- if (inQuote === false && (cur === ',' || cur === "\n")) {
- field = processField(field);
- // Add the current field to the current row
- row.push(field);
- // If this is EOR append row to output and flush row
- if (cur === "\n") {
- out.push(row);
- row = [];
- }
- // Flush the field buffer
- field = '';
- fieldQuoted = false;
- } else {
- // If it's not a ", add it to the field buffer
- if (cur !== '"') {
- field += cur;
- } else {
- if (!inQuote) {
- // We are not in a quote, start a quote
- inQuote = true;
- fieldQuoted = true;
- } else {
- // Next char is ", this is an escaped "
- if (s.charAt(i + 1) === '"') {
- field += '"';
- // Skip the next char
- i += 1;
- } else {
- // It's not escaping, so end quote
- inQuote = false;
- }
- }
- }
- }
- }
-
- // Add the last field
- field = processField(field);
- row.push(field);
- out.push(row);
-
- return out;
- };
-
- var rxIsInt = /^\d+$/,
- rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,
- // If a string has leading or trailing space,
- // contains a comma double quote or a newline
- // it needs to be quoted in CSV output
- rxNeedsQuoting = /^\s|\s$|,|"|\n/,
- trim = (function () {
- // Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists
- if (String.prototype.trim) {
- return function (s) {
- return s.trim();
- };
- } else {
- return function (s) {
- return s.replace(/^\s*/, '').replace(/\s*$/, '');
- };
- }
- }());
-
- function chomp(s) {
- if (s.charAt(s.length - 1) !== "\n") {
- // Does not end with \n, just return string
- return s;
- } else {
- // Remove the \n
- return s.substring(0, s.length - 1);
- }
- }
+ // Convert unquoted numbers to their appropriate types
+ if (rxIsInt.test(field)) {
+ field = parseInt(field, 10);
+ } else if (rxIsFloat.test(field)) {
+ field = parseFloat(field, 10);
+ }
+ }
+ return field;
+ };
+
+ for (i = 0; i < s.length; i += 1) {
+ cur = s.charAt(i);
+
+ // If we are at a EOF or EOR
+ if (inQuote === false && (cur === separator || cur === "\n")) {
+ field = processField(field);
+ // Add the current field to the current row
+ row.push(field);
+ // If this is EOR append row to output and flush row
+ if (cur === "\n") {
+ out.push(row);
+ row = [];
+ }
+ // Flush the field buffer
+ field = '';
+ fieldQuoted = false;
+ } else {
+ // If it's not a delimiter, add it to the field buffer
+ if (cur !== delimiter) {
+ field += cur;
+ } else {
+ if (!inQuote) {
+ // We are not in a quote, start a quote
+ inQuote = true;
+ fieldQuoted = true;
+ } else {
+ // Next char is delimiter, this is an escaped delimiter
+ if (s.charAt(i + 1) === delimiter) {
+ field += delimiter;
+ // Skip the next char
+ i += 1;
+ } else {
+ // It's not escaping, so end quote
+ inQuote = false;
+ }
+ }
+ }
+ }
+ }
+
+ // Add the last field
+ field = processField(field);
+ row.push(field);
+ out.push(row);
+
+ return out;
+ };
+
+ var rxIsInt = /^\d+$/,
+ rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,
+ // If a string has leading or trailing space,
+ // contains a comma double quote or a newline
+ // it needs to be quoted in CSV output
+ rxNeedsQuoting = /^\s|\s$|,|"|\n/,
+ trim = (function () {
+ // Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists
+ if (String.prototype.trim) {
+ return function (s) {
+ return s.trim();
+ };
+ } else {
+ return function (s) {
+ return s.replace(/^\s*/, '').replace(/\s*$/, '');
+ };
+ }
+ }());
+
+ function chomp(s) {
+ if (s.charAt(s.length - 1) !== "\n") {
+ // Does not end with \n, just return string
+ return s;
+ } else {
+ // Remove the \n
+ return s.substring(0, s.length - 1);
+ }
+ }
}(jQuery, this.recline.Backend));
@@ -3110,7 +3602,7 @@ this.recline.Backend = this.recline.Backend || {};
if (!metadata.id) {
metadata.id = String(Math.floor(Math.random() * 100000000) + 1);
}
- var backend = recline.Model.backends['memory'];
+ var backend = new recline.Backend.Memory();
var datasetInfo = {
documents: data,
metadata: metadata
@@ -3125,7 +3617,7 @@ this.recline.Backend = this.recline.Backend || {};
}
}
backend.addDataset(datasetInfo);
- var dataset = new recline.Model.Dataset({id: metadata.id}, 'memory');
+ var dataset = new recline.Model.Dataset({id: metadata.id}, backend);
dataset.fetch();
return dataset;
};
@@ -3160,6 +3652,8 @@ this.recline.Backend = this.recline.Backend || {};
// etc ...
// </pre>
my.Memory = my.Base.extend({
+ __type__: 'memory',
+ readonly: false,
initialize: function() {
this.datasets = {};
},
@@ -3207,13 +3701,9 @@ this.recline.Backend = this.recline.Backend || {};
var out = {};
var numRows = queryObj.size;
var start = queryObj.from;
- results = this.datasets[model.id].documents;
- _.each(queryObj.filters, function(filter) {
- results = _.filter(results, function(doc) {
- var fieldId = _.keys(filter.term)[0];
- return (doc[fieldId] == filter.term[fieldId]);
- });
- });
+ var results = this.datasets[model.id].documents;
+ results = this._applyFilters(results, queryObj);
+ results = this._applyFreeTextQuery(model, results, queryObj);
// not complete sorting!
_.each(queryObj.sort, function(sortObj) {
var fieldName = _.keys(sortObj)[0];
@@ -3231,6 +3721,43 @@ this.recline.Backend = this.recline.Backend || {};
return dfd.promise();
},
+ // in place filtering
+ _applyFilters: function(results, queryObj) {
+ _.each(queryObj.filters, function(filter) {
+ results = _.filter(results, function(doc) {
+ var fieldId = _.keys(filter.term)[0];
+ return (doc[fieldId] == filter.term[fieldId]);
+ });
+ });
+ return results;
+ },
+
+ // we OR across fields but AND across terms in query string
+ _applyFreeTextQuery: function(dataset, results, queryObj) {
+ if (queryObj.q) {
+ var terms = queryObj.q.split(' ');
+ results = _.filter(results, function(rawdoc) {
+ var matches = true;
+ _.each(terms, function(term) {
+ var foundmatch = false;
+ dataset.fields.each(function(field) {
+ var value = rawdoc[field.id];
+ if (value !== null) { value = value.toString(); }
+ // TODO regexes?
+ foundmatch = foundmatch || (value === term);
+ // TODO: early out (once we are true should break to spare unnecessary testing)
+ // if (foundmatch) return true;
+ });
+ matches = matches && foundmatch;
+ // TODO: early out (once false should break to spare unnecessary testing)
+ // if (!matches) return false;
+ });
+ return matches;
+ });
+ }
+ return results;
+ },
+
_computeFacets: function(documents, queryObj) {
var facetResults = {};
if (!queryObj.facets) {
@@ -3267,6 +3794,5 @@ this.recline.Backend = this.recline.Backend || {};
return facetResults;
}
});
- recline.Model.backends['memory'] = new my.Memory();
}(jQuery, this.recline.Backend));
diff --git a/ckan/templates/_snippet/data-viewer-embed-branded-link.html b/ckan/templates/_snippet/data-viewer-embed-branded-link.html
new file mode 100644
index 0000000..87eceb6
--- /dev/null
+++ b/ckan/templates/_snippet/data-viewer-embed-branded-link.html
@@ -0,0 +1,19 @@
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:i18n="http://genshi.edgewall.org/i18n"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ py:strip=""
+ >
+
+<div class="branded-link">
+ <p>Powered by <a href="${h.url_for(controller='package', action='resource_read', id=c.package.id, resource_id=c.resource.id)}">
+
+ ${g.site_title}
+ <img width="64" src="${h.url_for_static(g.site_logo)}" alt="${g.site_title} Logo" title="${g.site_title} Logo" id="logo" />
+ </a>
+ </p>
+</div>
+
+</html>
+
diff --git a/ckan/templates/_snippet/data-viewer-embed-dialog.html b/ckan/templates/_snippet/data-viewer-embed-dialog.html
new file mode 100644
index 0000000..b0b382d
--- /dev/null
+++ b/ckan/templates/_snippet/data-viewer-embed-dialog.html
@@ -0,0 +1,31 @@
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:i18n="http://genshi.edgewall.org/i18n"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ py:strip=""
+ >
+
+<div class="modal-data-viewer-embed-dialog modal fade in" style="display: none;"
+ py:def="data_viewer_embed_dialog()">
+ <div class="modal-header">
+ <a class="close" data-dismiss="modal">×</a>
+ <h3>
+ Embed Data Viewer
+ </h3>
+ </div>
+ <div class="modal-body">
+ <div>
+ <p><strong>Embed this view</strong> by copying this into your webpage:</p>
+ <textarea class="embedIframeText" style="width: 100%; height: 200px;"></textarea>
+ <p>Choose width and height in pixels:</p>
+ <label for="iframe-width">Width:</label>
+ <input class="iframe-width" name="iframe-width" value="800"/>
+ <label for="iframe-height">Height:</label>
+ <input class="iframe-height" name="iframe-height" value="500"/>
+ </div>
+ <a class="embedLink" href="">Preview</a>
+ </div>
+</div>
+
+</html>
diff --git a/ckan/templates/package/resource_embedded_dataviewer.html b/ckan/templates/package/resource_embedded_dataviewer.html
new file mode 100644
index 0000000..c03f4b9
--- /dev/null
+++ b/ckan/templates/package/resource_embedded_dataviewer.html
@@ -0,0 +1,98 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:i18n="http://genshi.edgewall.org/i18n"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ py:strip="">
+
+ <py:def function="optional_head">
+ <!-- data preview -->
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.css')}" />
+ <!--[if lte IE 8]>
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.ie.css')}" />
+ <![endif]-->
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/data-explorer.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/map.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/grid.css')}" />
+ <style type="text/css">
+
+ /* Hide the query controls */
+ .header {
+ display: none;
+ }
+
+ /* Hide CKAN footer */
+ .footer.outer {
+ display: none;
+ }
+
+ /* Don't center the main container. And provide a little space to the
+ left and above the viewer. This is for the graph-view, which, if a
+ small amount of room is not given, the y-axis' labels are uncomfortably
+ close to the edge of the viewport.
+ */
+ #main.container {
+ width: auto;
+ margin-left: 2px;
+ }
+
+ /* Remove the border from the right-hand-side */
+ #content {
+ border: 0px;
+ }
+
+ #ckanext-datapreview {
+ width: ${c.width-2}px;
+ height: ${c.height-115}px;
+ }
+
+ .recline-grid-container {
+ height: ${c.height-115}px;
+ }
+
+ .recline-graph .graph {
+ height: ${c.height-115}px;
+ }
+
+ .recline-map .map {
+ height: ${c.height-115}px;
+ }
+
+ .branded-link {
+ height: 70px;
+ }
+
+ .alert-messages {
+ display: none;
+ }
+
+ </style>
+ <script type="text/javascript">
+ var preload_resource = ${h.literal(c.resource_json)};
+ var reclineState = ${h.literal(c.recline_state)};
+ </script>
+ </py:def>
+
+ <py:def function="page_title">
+ ${h.dataset_display_name(c.package)} /
+ ${h.resource_display_name(c.resource)} - Dataset - Resource
+ </py:def>
+
+ <div py:match="content">
+ <div class="resource-preview">
+ <div id="ckanext-datapreview"></div>
+ </div>
+ <xi:include href="../_snippet/data-viewer-embed-branded-link.html" />
+ </div>
+
+ <py:def function="optional_footer">
+ <!-- data preview -->
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/jquery.mustache/jquery.mustache.js')}"></script>
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js')}"></script>
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js')}"></script>
+ <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.js')}"></script>
+ <script src="${h.url_for_static('/scripts/vendor/recline/recline.js')}"></script>
+ </py:def>
+
+ <xi:include href="../layout_base.html" />
+</html>
diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html
index b6c30e6..c99805a 100644
--- a/ckan/templates/package/resource_read.html
+++ b/ckan/templates/package/resource_read.html
@@ -12,6 +12,7 @@
py:strip="">
<xi:include href="../_snippet/data-api-help.html" />
+ <xi:include href="../_snippet/data-viewer-embed-dialog.html" />
<py:def function="optional_head">
<!-- data preview -->
@@ -20,8 +21,9 @@
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.ie.css')}" />
<![endif]-->
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/data-explorer.css')}" />
- <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph-flot.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph.css')}" />
<link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/map.css')}" />
+ <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/grid.css')}" />
<style type="text/css">
.recline-query-editor form, .recline-query-editor .text-query {
height: 28px;
@@ -58,9 +60,23 @@
.resource-actions .download img {
margin: 0px 4px -4px 0;
}
+ .preview-header {
+ padding-bottom: 13px;
+ padding-top: 0px;
+ }
+ .preview-header h3 {
+ display: inline;
+ }
+ .preview-header .btn {
+ float: right;
+ position: relative;
+ bottom: 6px;
+ padding: 8px 15px;
+ }
</style>
<script type="text/javascript">
var preload_resource = ${h.literal(c.resource_json)};
+ var embedPath = "${g.site_url+h.url_for(controller='package', action='resource_embedded_dataviewer', id=c.package.id, resource_id=c.resource.id)}";
</script>
</py:def>
@@ -104,6 +120,8 @@
${data_api_help(c.datastore_api)}
</py:if>
+ ${data_viewer_embed_dialog()}
+
<div class="quick-info">
<dl>
<dt>Last updated</dt>
@@ -151,7 +169,11 @@
</div>
<div class="resource-preview">
- <h3>Preview</h3>
+ <div class="preview-header">
+ <h3>Preview</h3>
+ <a py:if="c.pkg.is_private" title="Cannot embed as resource is private." style="display: none;" class="btn disabled" data-toggle="modal" href=".modal-data-viewer-embed-dialog">Embed</a>
+ <a py:if="not c.pkg.is_private" style="display: none;" class="btn btn-primary" data-toggle="modal" href=".modal-data-viewer-embed-dialog">Embed</a>
+ </div>
<div id="ckanext-datapreview"></div>
</div>
================================================================
Compare: https://github.com/okfn/ckan/compare/3e5b2f5...2821abf
More information about the ckan-changes
mailing list