Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
@@ -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);
}
});
@@ -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>
@@ -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;
}
});