Initial commit
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const randomQuote = require('builder/components/loading/random-quote');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const loadingTemplate = require('builder/components/loading/loading.tpl');
|
||||
const failTemplate = require('dashboard/components/fail.tpl');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'modalModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* shows a dialog to get the table exported
|
||||
* new ExportDialog({
|
||||
* table: table_model
|
||||
* })
|
||||
*
|
||||
* (Migrated almost-as-is from old editor to not break functionality)
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
_CSV_FILTER: 'SELECT * FROM (%%sql%%) as subq ',
|
||||
_MAX_SQL_GET_LENGTH: 1000,
|
||||
events: {
|
||||
'click .js-option:not(.is-disabled)': '_export',
|
||||
'click .js-cancel': '_closeDialog'
|
||||
},
|
||||
|
||||
/**
|
||||
* Allowed formats on the exporter
|
||||
* @type {Array}
|
||||
*/
|
||||
formats: [
|
||||
{ format: 'csv', fetcher: 'fetchCSV', geomRequired: false, illustrationIconModifier: 'IllustrationIcon--neutral' },
|
||||
{ format: 'shp', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--magenta' },
|
||||
{ format: 'kml', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--sunrise' },
|
||||
{ format: 'geojson', label: 'geo json', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--cyan' },
|
||||
{ format: 'svg', fetcher: 'fetchSVG', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--royalDark' },
|
||||
{ format: 'gpkg', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--smaltBlue' }
|
||||
],
|
||||
|
||||
initialize: function () {
|
||||
checkAndBuildOpts(this.options, REQUIRED_OPTS, this);
|
||||
_.extend(this.options, {
|
||||
clean_on_hide: true,
|
||||
table_id: this.model.id
|
||||
});
|
||||
_.bindAll(this, '_export');
|
||||
this.baseUrl = this._configModel.getSqlApiUrl();
|
||||
this.model.bind('change:geometry_types', this.refresh, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* search a format based on its name in the format array
|
||||
* @param {string} format Format name
|
||||
* @return {Object}
|
||||
*/
|
||||
getFormat: function (format) {
|
||||
for (var n in this.formats) {
|
||||
if (this.formats[n].format === format) {
|
||||
return this.formats[n];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Answer to button event and lauchn the export method associated to that format
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_export: function (ev) {
|
||||
this.killEvent(ev);
|
||||
var $button = $(ev.currentTarget);
|
||||
var formatName = $button.data('format');
|
||||
var format = this.getFormat(formatName);
|
||||
this[format.fetcher](formatName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a dictionary with the options shared between all the methods
|
||||
* @return {Object}
|
||||
*/
|
||||
getBaseOptions: function () {
|
||||
var options = {};
|
||||
options.filename = this.model.get('name');
|
||||
|
||||
if (this.options.user_data) {
|
||||
options.api_key = this.options.user_data.api_key;
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the base sql to retrieve the data
|
||||
* @return {string}
|
||||
*/
|
||||
getPlainSql: function () {
|
||||
throw new Error('Method not migrated, only used in subclass');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a specific sql filtered by the_geom, used on CSV exports
|
||||
* @return {string}
|
||||
*/
|
||||
getGeomFilteredSql: function () {
|
||||
var sql = this.getPlainSql();
|
||||
// if we have "the_geom" in our current schema, we apply a custom sql
|
||||
if (this.model.isGeoreferenced()) {
|
||||
return this._CSV_FILTER.replace(/%%sql%%/g, sql);
|
||||
}
|
||||
// Otherwise, we apply regular sql
|
||||
return sql;
|
||||
},
|
||||
|
||||
/**
|
||||
* Populates the hidden form with the format related values and submits them to get the file
|
||||
* @param {Object} options Base options
|
||||
* @param {String} sql Sql of the document to be retrieved
|
||||
*/
|
||||
_fetch: function (options, sql) {
|
||||
this._showElAndHideRest('.js-preparing-download');
|
||||
this.$('.format').val(options.format);
|
||||
this.$('.q').val(sql);
|
||||
this.$('.filename').val(options.filename);
|
||||
this.$('.api_key').val(options.api_key);
|
||||
|
||||
if (options.format === 'csv') {
|
||||
this.$('.skipfields').val('the_geom_webmercator');
|
||||
} else {
|
||||
this.$('.skipfields').val('the_geom,the_geom_webmercator');
|
||||
}
|
||||
|
||||
// check if the sql is big or not, and send the request as a verb or other. This is a HACK.
|
||||
if (sql.length < this._MAX_SQL_GET_LENGTH) {
|
||||
var location = this.$('form').attr('action') + '?' + this.$('form').serialize();
|
||||
this._fetchGET(location);
|
||||
} else {
|
||||
// I can't find a way of making the iframe trigger load event when its get a form posted,
|
||||
// so we need to leave like it was until
|
||||
this.submit();
|
||||
}
|
||||
|
||||
this.$('.db').attr('disabled', 'disabled');
|
||||
this.$('.skipfields').attr('disabled', 'disabled');
|
||||
|
||||
if (this.options.autoClose) {
|
||||
this.close();
|
||||
this.trigger('generating', this.$('.js-preparing-download').html());
|
||||
}
|
||||
},
|
||||
|
||||
showError: function (error) {
|
||||
this.$('.js-error').html(
|
||||
failTemplate({
|
||||
msg: error
|
||||
})
|
||||
);
|
||||
this._showElAndHideRest('.js-error');
|
||||
},
|
||||
|
||||
_fetchGET: function (url) {
|
||||
function getError (content) {
|
||||
// sql api returns a json when it fails
|
||||
// but if the browser is running some plugin that
|
||||
// formats it, the window content is the html
|
||||
// so search for the word "error"
|
||||
var error = null;
|
||||
try {
|
||||
var json = JSON.parse(content);
|
||||
error = json.error[0];
|
||||
} catch (e) {
|
||||
if (content && content.indexOf('error') !== -1) {
|
||||
error = 'an error occurred';
|
||||
}
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
var checkInterval;
|
||||
|
||||
var w = window.open(url);
|
||||
w.onload = () => {
|
||||
clearInterval(checkInterval);
|
||||
var error = getError(w.document.body.textContent);
|
||||
if (error) {
|
||||
this.showError(error);
|
||||
} else {
|
||||
this._modalModel.destroy();
|
||||
}
|
||||
w.close();
|
||||
};
|
||||
window.focus();
|
||||
checkInterval = setInterval(() => {
|
||||
// safari needs to check the body because it never
|
||||
// calls onload
|
||||
if (w.closed || (w.document && w.document.body.textContent.length === 0)) {
|
||||
this._modalModel.destroy();
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* Submits the form. This method is separated to ease the testing
|
||||
*/
|
||||
submit: function () {
|
||||
this.$('form').submit();
|
||||
},
|
||||
|
||||
/**
|
||||
* Base fetch, for the formats that don't require special threatment
|
||||
* @param {String} formatName
|
||||
*/
|
||||
fetch: function (formatName) {
|
||||
var options = this.getBaseOptions();
|
||||
options.format = formatName;
|
||||
var sql = this.getPlainSql();
|
||||
this._fetch(options, sql);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the options needed for csv format and fetch the document
|
||||
* @param {String} formatName
|
||||
*/
|
||||
fetchCSV: function () {
|
||||
var options = this.getBaseOptions();
|
||||
options.format = 'csv';
|
||||
var sql = this.getGeomFilteredSql();
|
||||
this.$('.skipfields').removeAttr('disabled');
|
||||
this._fetch(options, sql);
|
||||
},
|
||||
/**
|
||||
* Gets the options needed for svg format and fetch the document
|
||||
* @param {String} formatName
|
||||
*/
|
||||
fetchSVG: function () {
|
||||
this.$('.db').removeAttr('disabled');
|
||||
this.fetch('svg');
|
||||
},
|
||||
/**
|
||||
* Formerly render_content
|
||||
* Returns the html populated with current data
|
||||
* @return {String}
|
||||
*/
|
||||
render: function () {
|
||||
throw new Error('Method not migrated, only used in subclass');
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
this.$('.content').html(this.render_content());
|
||||
},
|
||||
|
||||
_renderLoadingContent: function (title) {
|
||||
return loadingTemplate({
|
||||
title: title,
|
||||
descHTML: randomQuote()
|
||||
});
|
||||
},
|
||||
|
||||
_showElAndHideRest: function (classNameToShow) {
|
||||
[
|
||||
'.js-start',
|
||||
'.js-preparing-download',
|
||||
'.js-error'
|
||||
].forEach(function (className) {
|
||||
this.$(className)[ className === classNameToShow ? 'show' : 'hide' ]();
|
||||
}, this);
|
||||
}
|
||||
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<div class="js-start">
|
||||
<div class="CDB-Text Dialog-header u-inner">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
|
||||
<i class="CDB-IconFont CDB-IconFont-cloudDownArrow"></i>
|
||||
</div>
|
||||
<p class="Dialog-headerTitle">Export dataset</p>
|
||||
<p class="Dialog-headerText">Select the preferred file format.</p>
|
||||
<% if (!isGeoreferenced) { %>
|
||||
<p class="Dialog-headerText">To download any geospatial format like SHP, KML or GeoJSON don't forget to select the_geom on your query.</p>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="CDB-Text Dialog-body">
|
||||
<% if (hasBounds) { %>
|
||||
<div class="OptionCheck">
|
||||
<div class="Checkbox js-bounds">
|
||||
<button class="Checkbox-input is-checked"></button>
|
||||
<label class="Checkbox-label"><strong class="Checkbox-strong">Match rows with the map view.</strong> This option reduces file size.</label>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="OptionCards">
|
||||
<% _.each( formats, function( format ){ %>
|
||||
<div data-format="<%- format.format %>"
|
||||
class="js-option OptionCard OptionCard--onlyIcons <%
|
||||
if (isGeoreferenced === false && format.geomRequired === true) { %> is-disabled <% }
|
||||
%>">
|
||||
<div class="IllustrationIcon <%- format.illustrationIconModifier %>">
|
||||
<div class="IllustrationIcon-text"><%- format.label || format.format %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Dialog-footer Dialog-footer--simple u-inner">
|
||||
<button class="CDB-Text CDB-Button CDB-Button--secondary CDB-Size-medium u-upperCase cancel js-close">
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="CDB-Text js-preparing-download" style="display: none;">
|
||||
<%- preparingDownloadContent %>
|
||||
</div>
|
||||
|
||||
<div class="CDB-Text js-error" style="display: none;"></div>
|
||||
|
||||
<form class="hack" method="POST" action="<%- url %>">
|
||||
<input type="hidden" class="filename" name="filename" />
|
||||
<input type="hidden" class="q" name="q" />
|
||||
<input type="hidden" class="format" name="format" />
|
||||
<input type="hidden" class="bounds" name="bounds" />
|
||||
<input type="hidden" class="api api_key" name="api_key" />
|
||||
<input type="hidden" class="skipfields" name="skipfields" disabled="disabled" value="" />
|
||||
<input type="hidden" class="dp" name="dp" value="4" disabled="disabled" />
|
||||
</form>
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const ExportView = require('./export-view');
|
||||
const templatePublicExport = require('./public-export-template.tpl');
|
||||
|
||||
/**
|
||||
* Shows a dialog to get the public table exported
|
||||
*
|
||||
* new PublicExportView({
|
||||
* table: table_model
|
||||
* })
|
||||
*
|
||||
*/
|
||||
module.exports = ExportView.extend({
|
||||
|
||||
// Events have to be extended from export view parent
|
||||
events: {
|
||||
'click .js-option:not(.is-disabled)': '_export',
|
||||
'click .js-bounds': '_changeBounds',
|
||||
'click .cancel': '_cancel',
|
||||
'click .close': '_cancel'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
ExportView.prototype.initialize.call(this);
|
||||
this.model.set('bounds', this.options.bounds);
|
||||
this.model.bind('change:bounds', this._setBoundsCheckbox, this);
|
||||
},
|
||||
|
||||
_changeBounds: function () {
|
||||
this.model.set('bounds', !this.model.get('bounds'));
|
||||
},
|
||||
|
||||
_setBoundsCheckbox: function () {
|
||||
this.$('.js-bounds .Checkbox-input').toggleClass('is-checked', !!this.model.get('bounds'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the bounds option to download the intersect or all the geometries
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_toggleBounds: function (ev) {
|
||||
this.killEvent(ev);
|
||||
var $button = $(ev.currentTarget);
|
||||
var formatName = $button.data('format');
|
||||
var format = this.getFormat(formatName);
|
||||
this[format.fetcher](formatName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a dictionary with the options shared between all the methods
|
||||
* @return {Object}
|
||||
*/
|
||||
getBaseOptions: function () {
|
||||
var options = {};
|
||||
options.filename = this.model.get('name');
|
||||
|
||||
// Keep dataset part in user.dataset names
|
||||
if (options.filename.indexOf('.') !== -1) {
|
||||
options.filename = options.filename.split('.')[1];
|
||||
}
|
||||
|
||||
if (this.options.user_data) {
|
||||
options.api_key = this.options.user_data.api_key;
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the base sql to retrieve the data
|
||||
* @return {string}
|
||||
*/
|
||||
getPlainSql: function () {
|
||||
let sql;
|
||||
if (this.options.sql) {
|
||||
sql = this.options.sql;
|
||||
} else {
|
||||
if (this.model.sqlView && this.model.get('bounds')) {
|
||||
sql = this.model.sqlView.getSQL();
|
||||
} else {
|
||||
sql = 'select * from ' + this.model.get('name');
|
||||
}
|
||||
}
|
||||
return sql;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the html populated with current data
|
||||
* @return {String}
|
||||
*/
|
||||
render: function () {
|
||||
var isGeoreferenced = this.model.isGeoreferenced();
|
||||
var hasBounds = this.model.get('bounds');
|
||||
|
||||
if (_.isBoolean(isGeoreferenced)) {
|
||||
this.$el.html(templatePublicExport({
|
||||
preparingDownloadContent: this._renderLoadingContent('We are preparing your download. Depending on the size, it could take some time.'),
|
||||
formats: this.formats,
|
||||
url: this.baseUrl,
|
||||
isGeoreferenced: isGeoreferenced,
|
||||
hasBounds: hasBounds
|
||||
}));
|
||||
} else {
|
||||
this.$el.html(this._renderLoadingContent('Checking georeferences…'));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user