Initial commit
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./modal-export-data-format.tpl');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'format',
|
||||
'isDisabled',
|
||||
'isChecked'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
tagName: 'li',
|
||||
className: 'Modal-listFormItem',
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
this._initViews();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
format: this._format,
|
||||
isDisabled: this._isDisabled,
|
||||
isChecked: this._isChecked
|
||||
})
|
||||
);
|
||||
|
||||
this.$el.toggleClass('is-disabled', this._isDisabled);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
<input class="CDB-Radio js-format" type="radio" name="format" data-format="<%- format.format %>"
|
||||
<% if (isDisabled) { %>
|
||||
disabled
|
||||
<% } %>
|
||||
|
||||
<% if (isChecked) { %>
|
||||
checked
|
||||
<% } %>
|
||||
>
|
||||
<span class="u-iBlock CDB-Radio-face"></span>
|
||||
<label class="u-iBlock u-lSpace u-upperCase"><%- format.label || format.format %></label>
|
||||
@@ -0,0 +1,349 @@
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var renderLoading = require('builder/components/loading/render-loading');
|
||||
var ErrorView = require('builder/components/error/error-view');
|
||||
var template = require('./modal-export-data.tpl');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var MetricsTracker = require('builder/components/metrics/metrics-tracker');
|
||||
var MetricsTypes = require('builder/components/metrics/metrics-types');
|
||||
|
||||
var FormatView = require('./modal-export-data-format-view');
|
||||
|
||||
var MAX_SQL_GET_LENGTH = 1000;
|
||||
var CSV_FILTER = 'SELECT * FROM (%%sql%%) as subq ';
|
||||
var FORMATS = [
|
||||
{
|
||||
format: 'csv',
|
||||
fetcher: 'fetchCSV',
|
||||
geomRequired: false
|
||||
}, {
|
||||
format: 'shp',
|
||||
fetcher: 'fetch',
|
||||
geomRequired: true
|
||||
}, {
|
||||
format: 'kml',
|
||||
fetcher: 'fetch',
|
||||
geomRequired: true
|
||||
}, {
|
||||
format: 'geojson',
|
||||
label: 'geo json',
|
||||
fetcher: 'fetch',
|
||||
geomRequired: true
|
||||
}, {
|
||||
format: 'svg',
|
||||
fetcher: 'fetchSVG',
|
||||
geomRequired: true
|
||||
}, {
|
||||
format: 'gpkg',
|
||||
fetcher: 'fetch',
|
||||
geomRequired: true
|
||||
}
|
||||
];
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'queryGeometryModel',
|
||||
'querySchemaModel',
|
||||
'canHideColumns',
|
||||
'modalModel',
|
||||
'fromView'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'Dialog-content',
|
||||
|
||||
events: {
|
||||
'click .js-confirm': '_onConfirm',
|
||||
'click .js-cancel': '_close'
|
||||
},
|
||||
|
||||
options: {
|
||||
autoClose: true
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
this._layerModel = opts.layerModel;
|
||||
this._filename = opts.filename;
|
||||
|
||||
this._initBinds();
|
||||
|
||||
if (this._queryGeometryModel.get('status') === 'unfetched') {
|
||||
this._queryGeometryModel.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
if (this._queryGeometryModel.get('status') === 'fetched' || this._queryGeometryModel.get('status') === 'unavailable') {
|
||||
this._renderFormats();
|
||||
} else {
|
||||
this._renderLoadingContent(_t('components.modals.export-data.loading.geometry'));
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderFormats: function () {
|
||||
var geometry = this._queryGeometryModel.get('simple_geom');
|
||||
var isGeoreferenced = !!geometry;
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
isGeoreferenced: isGeoreferenced,
|
||||
url: this._configModel.getSqlApiUrl()
|
||||
})
|
||||
);
|
||||
|
||||
var isChecked = true;
|
||||
|
||||
_.each(FORMATS, function (format) {
|
||||
var isDisabled = (isGeoreferenced === false && format.geomRequired === true);
|
||||
if (isGeoreferenced === true && format.geomRestricted != null && !isDisabled) {
|
||||
isDisabled = !_.contains(format.geomRestricted, geometry);
|
||||
}
|
||||
|
||||
this._renderFormat(format, isDisabled, !isDisabled && isChecked);
|
||||
|
||||
if (!isDisabled) {
|
||||
isChecked = false;
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
_renderFormat: function (format, isDisabled, isChecked) {
|
||||
var view = new FormatView({
|
||||
format: format,
|
||||
isDisabled: isDisabled,
|
||||
isChecked: isChecked
|
||||
});
|
||||
|
||||
this.$('.js-formats').append(view.render().el);
|
||||
this.addView(view);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this._queryGeometryModel.bind('change:status', function () {
|
||||
if (this._queryGeometryModel.get('status') !== 'unavailable') {
|
||||
this.render();
|
||||
} else {
|
||||
this.showError(_t('components.modals.export-data.error.geometry-error'));
|
||||
}
|
||||
}, this);
|
||||
this.add_related_model(this._queryGeometryModel);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 FORMATS) {
|
||||
if (FORMATS[n].format === format) {
|
||||
return FORMATS[n];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onConfirm: function () {
|
||||
this._form = this.$('.js-form');
|
||||
var formatName = $('.js-format[name=format]:checked', this._form).data('format');
|
||||
var format = this._getFormat(formatName);
|
||||
|
||||
if (this._layerModel) {
|
||||
MetricsTracker.track(MetricsTypes.DOWNLOADED_LAYER, {
|
||||
format: formatName,
|
||||
layer_id: this._layerModel.get('id'),
|
||||
source: this._layerModel.get('source'),
|
||||
visible: this._layerModel.get('visible'),
|
||||
table_name: this._layerModel.get('table_name'),
|
||||
from_view: this._fromView
|
||||
});
|
||||
}
|
||||
|
||||
this[format.fetcher](formatName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a dictionary with the options shared between all the methods
|
||||
* @return {Object}
|
||||
*/
|
||||
getBaseOptions: function () {
|
||||
return {
|
||||
filename: this._filename,
|
||||
apiKey: this._configModel.get('api_key')
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a specific sql filtered by the_geom, used on CSV exports
|
||||
* @return {string}
|
||||
*/
|
||||
getGeomFilteredSql: function () {
|
||||
var sql = this._queryGeometryModel.get('query');
|
||||
var geom = this._queryGeometryModel.get('simple_geom');
|
||||
|
||||
// if we have "the_geom" in our current schema, we apply a custom sql
|
||||
if (geom) {
|
||||
return 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.$('.js-format').val(options.format);
|
||||
this.$('.js-q').val(sql);
|
||||
this.$('.js-filename').val(options.filename);
|
||||
this.$('.js-apiKey').val(options.apiKey);
|
||||
|
||||
var skipFields = ['the_geom_webmercator'];
|
||||
|
||||
if (options.format !== 'csv') {
|
||||
skipFields.push('the_geom');
|
||||
}
|
||||
|
||||
// We remove the center column only if there's a time buffer analysis on this layer
|
||||
// and not a deprecated SQL function analysis. Time buffer always adds this column, but
|
||||
// a SQL analysis might add arbitrary columns.
|
||||
if (this._canHideColumns) {
|
||||
const columnNames = this._querySchemaModel.getColumnNames();
|
||||
// Skip column center if it's of type geometry
|
||||
if (columnNames.indexOf('center') !== -1 && this._querySchemaModel.getColumnType('center') === 'geometry') {
|
||||
skipFields.push('center');
|
||||
}
|
||||
}
|
||||
|
||||
this.$('.js-skipfields').val(skipFields.join(','));
|
||||
|
||||
// TODO: track metrics
|
||||
|
||||
// check if the sql is big or not, and send the request as a verb or other. This is a HACK.
|
||||
if (sql.length < MAX_SQL_GET_LENGTH) {
|
||||
var location = this.$('.js-form').attr('action') + '?' + this.$('.js-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.$('.js-skipfields').attr('disabled', 'disabled');
|
||||
|
||||
if (this.options.autoClose) {
|
||||
this._close();
|
||||
} else {
|
||||
this._renderLoadingContent(_t('components.modals.export-data.loading.preparing'));
|
||||
}
|
||||
},
|
||||
|
||||
showError: function (errorMessage) {
|
||||
var errorView = new ErrorView({
|
||||
title: _t('hello'),
|
||||
desc: errorMessage
|
||||
});
|
||||
this.$el.html(errorView.render().el);
|
||||
this.addView(errorView);
|
||||
},
|
||||
|
||||
_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 = _t('components.modals.export-data.error.unknown');
|
||||
}
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var checkInterval;
|
||||
|
||||
var w = window.open(url);
|
||||
w.onload = function () {
|
||||
clearInterval(checkInterval);
|
||||
var error = getError(w.document.body.textContent);
|
||||
if (error) {
|
||||
self.showError(error);
|
||||
} else {
|
||||
self._close();
|
||||
}
|
||||
w.close();
|
||||
};
|
||||
window.focus();
|
||||
checkInterval = setInterval(function check () {
|
||||
// safari needs to check the body because it never
|
||||
// calls onload
|
||||
if (w.closed || (w.document && w.document.body.textContent.length === 0)) {
|
||||
self._close();
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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._queryGeometryModel.get('query');
|
||||
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.$('.js-skipfields').removeAttr('disabled');
|
||||
this._fetch(options, sql);
|
||||
},
|
||||
/**
|
||||
* Gets the options needed for svg format and fetch the document
|
||||
* @param {String} formatName
|
||||
*/
|
||||
fetchSVG: function () {
|
||||
this.fetch('svg');
|
||||
},
|
||||
|
||||
/**
|
||||
* Submits the form. This method is separated to ease the testing
|
||||
*/
|
||||
submit: function () {
|
||||
this.$('.js-form').submit();
|
||||
},
|
||||
|
||||
_renderLoadingContent: function (title) {
|
||||
this.$el.html(
|
||||
renderLoading({
|
||||
title: title
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_close: function () {
|
||||
this._modalModel.destroy();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="u-flex u-justifyCenter">
|
||||
<div class="Modal-inner u-flex u-justifyCenter">
|
||||
<div class="Modal-icon">
|
||||
<svg width="24px" height="24px" viewbox="459 348 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Export-dataset" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(459.000000, 348.000000)">
|
||||
<path d="M1,1 L11,1 L11,5.5 C11,5.776 11.224,6 11.5,6 L16,6 L16,9.5 L17,9.5 L17,5.502 C17,5.502 17,5.502 17,5.501 C17,5.5 17,5.501 17,5.5 C17,5.447 16.985,5.398 16.97,5.35 C16.966,5.337 16.967,5.323 16.962,5.311 C16.936,5.247 16.896,5.19 16.847,5.142 L11.854,0.147 C11.808,0.101 11.753,0.064 11.692,0.039 C11.632,0.014 11.567,0 11.5,0 L0.5,0 C0.224,0 0,0.224 0,0.5 L0,21.5 C0,21.776 0.224,22 0.5,22 L10.5,22 L10.5,21 L1,21 L1,1 L1,1 Z M12,1.708 L15.291,5 L12,5 L12,1.708 L12,1.708 Z" id="Shape" fill="#2E3C43"/>
|
||||
<path d="M17.5,11 C13.916,11 11,13.916 11,17.5 C11,21.084 13.916,24 17.5,24 C21.084,24 24,21.084 24,17.5 C24,13.916 21.084,11 17.5,11 L17.5,11 Z M17.5,23 C14.467,23 12,20.533 12,17.5 C12,14.467 14.467,12 17.5,12 C20.533,12 23,14.467 23,17.5 C23,20.533 20.533,23 17.5,23 L17.5,23 Z" id="Shape" fill="#2E3C43"/>
|
||||
<path d="M17.858,14.425 C17.767,14.331 17.641,14.272 17.5,14.272 C17.359,14.272 17.232,14.331 17.142,14.425 L14.965,16.602 C14.77,16.797 14.77,17.114 14.965,17.309 C15.16,17.504 15.477,17.504 15.672,17.309 L17,15.981 L17,20.226 C17,20.502 17.224,20.726 17.5,20.726 C17.776,20.726 18,20.502 18,20.226 L18,15.981 L19.328,17.309 C19.426,17.407 19.554,17.455 19.682,17.455 C19.81,17.455 19.938,17.406 20.036,17.309 C20.231,17.114 20.231,16.797 20.036,16.602 L17.858,14.425 L17.858,14.425 Z" id="Shape" fill="#2E3C43" transform="translate(17.500500, 17.499000) scale(1, -1) translate(-17.500500, -17.499000) "/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="CDB-Text CDB-Size-huge is-light u-bSpace--xl"><%- _t('components.modals.export-data.title') %></h2>
|
||||
<p class="CDB-Text CDB-Size-large u-altTextColor"><%- _t('components.modals.export-data.desc') %>.</p>
|
||||
<% if (!isGeoreferenced) { %>
|
||||
<p class="CDB-Text CDB-Size-large u-altTextColor u-tSpace-xl"><%- _t('components.modals.export-data.no-geometry') %></p>
|
||||
<% } %>
|
||||
|
||||
<div class="Modal-listTextItem u-flex u-alignCenter">
|
||||
<h3 class="CDB-Text CDB-Size-medium is-semibold u-upperCase">Select format</h3>
|
||||
|
||||
<form class="js-form" method="POST" action="<%- url %>">
|
||||
<input type="hidden" class="js-filename" name="filename" />
|
||||
<input type="hidden" class="js-q" name="q" />
|
||||
<input type="hidden" class="js-apiKey" name="api_key" />
|
||||
<input type="hidden" class="js-skipfields" name="skipfields" disabled="disabled" value="" />
|
||||
<input type="hidden" class="js-dp" name="dp" value="4" disabled="disabled" />
|
||||
|
||||
<ul class="Modal-listForm u-flex u-alignCenter CDB-Text CDB-Size-medium js-formats"></ul>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<ul class="Modal-listActions u-flex u-alignCenter">
|
||||
<li class="Modal-listActionsitem">
|
||||
<button class="CDB-Button CDB-Button--secondary CDB-Button--big js-cancel">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.export-data.cancel') %></span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="Modal-listActionsitem">
|
||||
<button class="CDB-Button CDB-Button--primary CDB-Button--big js-confirm">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.export-data.download') %></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user