1090 lines
32 KiB
JavaScript
1090 lines
32 KiB
JavaScript
/**
|
|
* models for cartodb admin
|
|
*/
|
|
|
|
(function() {
|
|
|
|
cdb.admin.SQL = function() {
|
|
var username = (this.options && this.options.user_data)? this.options.user_data.username :
|
|
(window.user_data? window.user_data.username : window.user_name);
|
|
var api_key = (this.options && this.options.user_data)? this.options.user_data.api_key :
|
|
(window.user_data? window.user_data.api_key : window.api_key);
|
|
|
|
|
|
return new cartodb.SQL({
|
|
user: username,
|
|
api_key: api_key,
|
|
sql_api_template: cdb.config.getSqlApiBaseUrl()
|
|
});
|
|
}
|
|
|
|
cdb.admin.Column = cdb.core.Model.extend({
|
|
|
|
idAttribute: 'name',
|
|
|
|
url: function(method) {
|
|
var version = cdb.config.urlVersion('column', method);
|
|
var table = this.table || this.collection.table;
|
|
if(!table) {
|
|
cdb.log.error("column has no table assigned");
|
|
}
|
|
|
|
var base = '/api/' + version + '/tables/' + table.get('name') + '/columns/';
|
|
if (this.isNew()) {
|
|
return base;
|
|
}
|
|
return base + this.id;
|
|
},
|
|
|
|
|
|
initialize: function() {
|
|
this.table = this.get('table');
|
|
if(!this.table) {
|
|
throw "you should specify a table model";
|
|
}
|
|
this.unset('table', { silent: true });
|
|
},
|
|
|
|
toJSON: function() {
|
|
var c = _.clone(this.attributes);
|
|
// this hack is created to create new column
|
|
// if you set _name instead name backbone does not get
|
|
// it as idAttribute so launch a POST instead of a PUT
|
|
if(c._name) {
|
|
c.name = c._name;
|
|
delete c._name;
|
|
}
|
|
return c;
|
|
},
|
|
|
|
});
|
|
|
|
|
|
/**
|
|
* contains information about the table, not the data itself
|
|
*/
|
|
cdb.admin.CartoDBTableMetadata = cdb.ui.common.TableProperties.extend({
|
|
|
|
currentLoading: 0, // class variable (shared). I'm still not sure if this is messy as hell or powerfull as a transformer
|
|
sqlApiClass: cartodb.SQL,
|
|
|
|
_TEXTS: {
|
|
columnDeleted: 'Your column has been deleted',
|
|
columnDeleting: 'Deleting your column',
|
|
columnAdded: 'Your column has been added',
|
|
columnAdding: 'Adding new column'
|
|
},
|
|
|
|
hiddenColumns: [
|
|
'the_geom',
|
|
'the_geom_webmercator',
|
|
'cartodb_georef_status',
|
|
'created_at',
|
|
'updated_at',
|
|
'cartodb_id'
|
|
],
|
|
|
|
initialize: function() {
|
|
_.bindAll(this, 'notice');
|
|
this.readOnly = false;
|
|
this.bind('change:schema', this._prepareSchema, this);
|
|
this._prepareSchema();
|
|
this.sqlView = null;
|
|
this.synchronization = new cdb.admin.TableSynchronization();
|
|
this.synchronization.linkToTable(this);
|
|
this.synchronization.bind('change:id', function isSyncChanged() {
|
|
this.trigger('change:isSync', this, this.synchronization.isSync());
|
|
}, this);
|
|
if (this.get('no_data_fetch')) {
|
|
this.no_data_fetch = true;
|
|
delete this.attributes.no_data_fetch;
|
|
}
|
|
this.data();
|
|
this.bind('error', function(e, resp) {
|
|
this.error('', resp);
|
|
}, this);
|
|
this._data.bind('error', function(e, resp) {
|
|
this.notice('error loading rows, check your SQL query', 'error', 5000);
|
|
}, this);
|
|
|
|
this._data.bind('reset', function() {
|
|
var view = this._data;
|
|
this.set({
|
|
schema: view.schemaFromData(this.get('schema')),
|
|
geometry_types: view.getGeometryTypes()
|
|
});
|
|
}, this);
|
|
|
|
this.retrigger('change', this._data, 'data:changed');
|
|
this.retrigger('saved', this._data, 'data:saved');
|
|
|
|
this.bind('change:table_visualization', function() {
|
|
this.permission = new cdb.admin.Permission(this.get('table_visualization').permission);
|
|
this.trigger('change:permission', this, this.permission);
|
|
}, this);
|
|
|
|
// create permission if permission is set
|
|
this.permission = new cdb.admin.Permission(this.get('permission'));
|
|
},
|
|
|
|
url: function(method) {
|
|
var version = cdb.config.urlVersion('table', method);
|
|
var base = '/api/' + version + '/tables';
|
|
if (this.isNew()) {
|
|
return base;
|
|
}
|
|
return base + '/' + this.id;
|
|
},
|
|
|
|
// use the name as the id since the api works
|
|
// in the same way to table name and id
|
|
parse: function(resp, xhr) {
|
|
if(resp.name) {
|
|
resp.id = resp.name;
|
|
}
|
|
// move geometry_types to stats one
|
|
// geometry_types from backend are not reliable anymore and it can only be used
|
|
// for non editing stuff (showing icons, general checks on table list)
|
|
resp.stats_geometry_types = resp.geometry_types;
|
|
delete resp.geometry_types;
|
|
delete resp.schema;
|
|
return resp;
|
|
},
|
|
|
|
notice: function(msg, type, timeout) {
|
|
this.trigger('notice', msg, type, timeout);
|
|
},
|
|
|
|
setReadOnly: function(_) {
|
|
var trigger = false;
|
|
if (this.readOnly !== _) {
|
|
trigger = true;
|
|
}
|
|
this.readOnly = _;
|
|
if (trigger) {
|
|
this.trigger('change:readOnly', this, _);
|
|
}
|
|
},
|
|
|
|
isReadOnly: function() {
|
|
return this.readOnly || this.data().isReadOnly() || this.synchronization.isSync();
|
|
},
|
|
|
|
isSync: function() {
|
|
return this.synchronization.isSync();
|
|
},
|
|
|
|
getUnqualifiedName: function() {
|
|
var name = this.get('name');
|
|
if (!name) return null;
|
|
var tk = name.split('.');
|
|
if (tk.length == 2) {
|
|
return tk[1];
|
|
}
|
|
return name;
|
|
},
|
|
|
|
// "user".table -> user.table
|
|
getUnquotedName: function() {
|
|
var name = this.get('name');
|
|
return name && name.replace(/"/g, '');
|
|
},
|
|
|
|
sortSchema: function() {
|
|
this.set('schema', cdb.admin.CartoDBTableMetadata.sortSchema(this.get('schema')));
|
|
},
|
|
|
|
error: function(msg, resp) {
|
|
var err = resp && resp.responseText && JSON.parse(resp.responseText).errors[0];
|
|
this.trigger('notice', msg + ": " + err, 'error');
|
|
},
|
|
|
|
_prepareSchema: function() {
|
|
var self = this;
|
|
this._columnType = {};
|
|
|
|
_(this.get('schema')).each(function(s) {
|
|
self._columnType[s[0]] = s[1];
|
|
});
|
|
|
|
if (!this.isInSQLView()) {
|
|
self.set('original_schema', self.get('schema'));
|
|
}
|
|
},
|
|
|
|
columnNames: function(sc) {
|
|
sc = sc || 'schema'
|
|
return _(this.get(sc)).pluck(0);
|
|
},
|
|
|
|
containsColumn: function(name) {
|
|
return _.contains(this.columnNames(), name);
|
|
},
|
|
|
|
columnNamesByType: function(type, sc) {
|
|
sc = sc || 'schema';
|
|
var t = _(this.get(sc)).filter(function(c) {
|
|
return c[1] == type;
|
|
});
|
|
return _(t).pluck(0);
|
|
},
|
|
|
|
// return geometry columns calculated backend stats
|
|
// use geomColumnTypes if you need something reliable (but slower and async)
|
|
statsGeomColumnTypes: function(geometryTypes) {
|
|
return this.geomColumnTypes(this.get('stats_geometry_types'))
|
|
},
|
|
|
|
// return the current column types in an array
|
|
// the values inside the array can be:
|
|
// 'point', 'line', 'polygon'
|
|
geomColumnTypes: function(geometryTypes) {
|
|
var types = geometryTypes || this.get('geometry_types');
|
|
var geomTypes = [];
|
|
if (!_.isArray(types)) {
|
|
return [];
|
|
}
|
|
var _map = {
|
|
'st_multipolygon': 'polygon',
|
|
'st_polygon': 'polygon',
|
|
'st_multilinestring': 'line',
|
|
'st_linestring': 'line',
|
|
'st_multipoint': 'point',
|
|
'st_point': 'point'
|
|
};
|
|
for(var t in types) {
|
|
var type = types[t];
|
|
// when there are rows with no geo type null is returned as geotype
|
|
if(type) {
|
|
var a = _map[type.toLowerCase()]
|
|
if(a) {
|
|
geomTypes.push(a)
|
|
}
|
|
}
|
|
}
|
|
return _.uniq(geomTypes);
|
|
},
|
|
|
|
/**
|
|
* Adding a new geometry type to the table
|
|
* @param geom type {st_polygon, st_point,...}
|
|
* @param set options
|
|
*/
|
|
addGeomColumnType: function(t, opts) {
|
|
if(!t) return;
|
|
var types = _.clone(this.get('geometry_types')) || [];
|
|
if(!_.contains(types, t)) {
|
|
types.push(t);
|
|
|
|
this.set({
|
|
'geometry_types': types
|
|
}, opts);
|
|
}
|
|
},
|
|
|
|
nonReservedColumnNames: function() {
|
|
|
|
var self = this;
|
|
return _.filter(this.columnNames(), function(c) {
|
|
return !self.isReservedColumn(c);
|
|
});
|
|
},
|
|
|
|
columnTypes: function() {
|
|
return _.clone(this._columnType);
|
|
},
|
|
|
|
_getColumn: function(columnName) {
|
|
if(this._columnType[columnName] === undefined) {
|
|
return
|
|
// throw "the column does not exists";
|
|
}
|
|
var c = new cdb.admin.Column({
|
|
table: this,
|
|
name: columnName,
|
|
type: this._columnType[columnName]
|
|
});
|
|
return c;
|
|
},
|
|
|
|
getColumnType: function(columnName, sc) {
|
|
sc = sc || 'schema';
|
|
var t = _(this.get(sc)).filter(function(c) {
|
|
return c[0] == columnName;
|
|
});
|
|
if(t.length > 0)
|
|
return t[0][1];
|
|
return;
|
|
},
|
|
|
|
addColumn: function(columnName, columnType, opts) {
|
|
var self = this;
|
|
var c = new cdb.admin.Column({
|
|
table: this,
|
|
_name: columnName,
|
|
type: columnType || 'string'
|
|
});
|
|
this.notice(self._TEXTS.columnAdding, 'load', 0);
|
|
c.save(null, {
|
|
success: function(mdl, obj) {
|
|
self.notice(self._TEXTS.columnAdded, 'info');
|
|
self.trigger('columnAdd', columnName);
|
|
self.data().fetch();
|
|
opts && opts.success && opts.success(mdl,obj);
|
|
},
|
|
error: function(e, resp) {
|
|
self.error('error adding column', resp);
|
|
opts && opts.error && opts.error(e);
|
|
},
|
|
wait: true
|
|
});
|
|
},
|
|
|
|
deleteColumn: function(columnName, opts) {
|
|
var self = this;
|
|
var c = this._getColumn(columnName);
|
|
if (c !== undefined) {
|
|
this.notice(self._TEXTS.columnDeleting, 'load', 0);
|
|
c.destroy({
|
|
success: function() {
|
|
self.trigger('columnDelete', columnName);
|
|
self.notice(self._TEXTS.columnDeleted, 'info');
|
|
self.data().fetch();
|
|
opts && opts.success && opts.success();
|
|
},
|
|
error: function(e, resp) {
|
|
self.error('error deleting column', resp);
|
|
opts && opts.error && opts.error();
|
|
},
|
|
wait: true
|
|
});
|
|
}
|
|
},
|
|
|
|
renameColumn: function(columnName, newName, opts) {
|
|
if(columnName == newName) return;
|
|
var self = this;
|
|
var c = this._getColumn(columnName);
|
|
var oldName = c.get('name');
|
|
c.set({
|
|
new_name: newName,
|
|
old_name: c.get('name')
|
|
});
|
|
this.notice('renaming column', 'load', 0);
|
|
c.save(null, {
|
|
success: function(mdl, data) {
|
|
self.notice('Column has been renamed', 'info');
|
|
self.trigger('columnRename', newName, oldName);
|
|
self.data().fetch();
|
|
opts && opts.success && opts.success(mdl, data);
|
|
},
|
|
error: function(e, resp) {
|
|
cdb.log.error("can't rename column");
|
|
self.error('error renaming column', resp);
|
|
opts && opts.error && opts.error(e, resp);
|
|
},
|
|
wait: true
|
|
});
|
|
},
|
|
|
|
isTypeChangeAllowed: function(columnName, newType) {
|
|
var deactivateMatrix = {
|
|
'number': ['date'],
|
|
'boolean': ['date'],
|
|
'date': ['boolean']
|
|
};
|
|
var c = this._getColumn(columnName);
|
|
if (!c) {
|
|
return true;
|
|
}
|
|
var type = c.get('type');
|
|
var deactivated = deactivateMatrix[type] || [];
|
|
deactivated = deactivated.concat([type])
|
|
return !_.contains(deactivated, newType);
|
|
},
|
|
|
|
isTypeChangeDestructive: function(columnName, newType) {
|
|
var columnType = this.getColumnType(columnName);
|
|
|
|
var destructiveMatrix = {
|
|
"string": {
|
|
"string": false,
|
|
"number": true,
|
|
"date": true,
|
|
"boolean": true,
|
|
},
|
|
"number": {
|
|
"string": false,
|
|
"number": false,
|
|
"date": true,
|
|
"boolean": true,
|
|
},
|
|
"date": {
|
|
"string": false,
|
|
"number": true,
|
|
"date": false,
|
|
"boolean": true,
|
|
},
|
|
"boolean": {
|
|
"string": false,
|
|
"number": false,
|
|
"date": true,
|
|
"boolean": false,
|
|
},
|
|
}
|
|
return destructiveMatrix[columnType][newType]
|
|
},
|
|
|
|
changeColumnType: function(columnName, newType, opts) {
|
|
var self = this;
|
|
var c = this._getColumn(columnName);
|
|
|
|
if(this.getColumnType(columnName) == newType) {
|
|
opts && opts.success && opts.success();
|
|
return;
|
|
}
|
|
this.saveNewColumnType(c, newType, opts);
|
|
},
|
|
|
|
saveNewColumnType: function(column, newType, opts) {
|
|
var self = this;
|
|
column.set({ type: newType});
|
|
this.notice('Changing column type', 'load', 0);
|
|
column.save(null, {
|
|
success: function() {
|
|
self.notice('Column type has been changed', 'info');
|
|
self.trigger('typeChanged', newType); // to make it testable
|
|
self.data().fetch();
|
|
opts && opts.success && opts.success();
|
|
},
|
|
error: function(e, resp) {
|
|
self.trigger('typeChangeFailed', newType, e); // to make it testable
|
|
self.error('error changing column type', resp);
|
|
opts && opts.error && opts.error(e, resp);
|
|
},
|
|
wait: true
|
|
});
|
|
},
|
|
|
|
/**
|
|
* returns the original data for the table not the current applied view
|
|
*/
|
|
originalData: function() {
|
|
return this._data;
|
|
},
|
|
|
|
data: function() {
|
|
var self = this;
|
|
if(this._data === undefined) {
|
|
this._data = new cdb.admin.CartoDBTableData(null, {
|
|
table: this
|
|
});
|
|
this.bindData();
|
|
}
|
|
if(this.sqlView) {
|
|
return this.sqlView;
|
|
}
|
|
return this._data;
|
|
},
|
|
|
|
bindData: function(data) {
|
|
var self = this;
|
|
if(this._data && !this._data.bindedReset) {
|
|
|
|
this.retrigger('reset', this._data, 'dataLoaded');
|
|
this.retrigger('add', this._data, 'dataAdded');
|
|
this._data.bindedReset = true;
|
|
|
|
}
|
|
if(this.sqlView && !this.sqlView.bindedReset) {
|
|
this.retrigger('reset', this.sqlView, 'dataLoaded');
|
|
this.retrigger('add', this.sqlView, 'dataAdded');
|
|
this.sqlView.bindedReset = true;
|
|
}
|
|
|
|
},
|
|
|
|
useSQLView: function(view, options) {
|
|
if (!view && !this.sqlView) return;
|
|
options = options || {};
|
|
var self = this;
|
|
var data = this.data();
|
|
|
|
if(this.sqlView) {
|
|
this.sqlView.unbind(null, null, this);
|
|
this.sqlView.unbind(null, null, this._data);
|
|
}
|
|
|
|
// reset previous
|
|
if(!view && this.sqlView) {
|
|
this.sqlView.table = null;
|
|
}
|
|
|
|
this.sqlView = view;
|
|
this.bindData();
|
|
|
|
if(view) {
|
|
view.bind('reset', function() {
|
|
if(!view.modify_rows) {
|
|
this.set({
|
|
schema: view.schemaFromData(this.get('schema')),
|
|
geometry_types: view.getGeometryTypes()
|
|
});
|
|
}
|
|
}, this);
|
|
// listen for errors
|
|
view.bind('error', function(e, resp) {
|
|
this.notice('error loading rows, check your SQL query', 'error', 5000);
|
|
}, this);
|
|
|
|
view.bind('loading', function() {
|
|
//this.notice(_t('loading query'), 'load', 0);
|
|
}, this);
|
|
|
|
view.bind('reset loaded', function() {
|
|
if(view.modify_rows) {
|
|
this.notice(view.affected_rows + ' rows affected');
|
|
this.useSQLView(null);
|
|
} else {
|
|
this.notice(_t('loaded'));
|
|
}
|
|
}, this);
|
|
|
|
// swicth source data
|
|
this.dataModel = this.sqlView;
|
|
view.table = this;
|
|
} else {
|
|
this.dataModel = this._data;
|
|
// get the original schema
|
|
self.set({
|
|
'schema': self.get('original_schema')
|
|
});///*, { silent: true });
|
|
self.data().fetch();
|
|
}
|
|
this.trigger('change:dataSource', this.dataModel, this);
|
|
},
|
|
|
|
isInSQLView: function() {
|
|
return this.sqlView ? true: false;
|
|
},
|
|
|
|
/**
|
|
* replace fetch functionally to add some extra call for logging
|
|
* it can be used in the same way fetch is
|
|
*/
|
|
fetch: function(opts) {
|
|
var self = this;
|
|
silent = opts? opts.silent : false;
|
|
if(!silent) {
|
|
this.notice('loading table', 'load', this, 0, 0);
|
|
}
|
|
var xhr = this.elder('fetch', opts)
|
|
$.when(xhr).done(function() {
|
|
opts && opts.success && opts.success.old_success && opts.success.old_success();
|
|
if(!silent) {
|
|
self.notice('loaded');
|
|
}
|
|
}).fail(function(){
|
|
if(!silent) {
|
|
self.notice('error loading the table');
|
|
}
|
|
});
|
|
return xhr;
|
|
|
|
},
|
|
|
|
isReservedColumn: function(c) {
|
|
return cdb.admin.Row.isReservedColumn(c);
|
|
},
|
|
|
|
/**
|
|
* when a table is linked to a infowindow each time a column
|
|
* is renamed or removed the table pings to infowindow to remove
|
|
* or rename the fields
|
|
*/
|
|
linkToInfowindow: function(infowindow) {
|
|
var self = this;
|
|
this.bind('columnRename', function(newName, oldName) {
|
|
if(infowindow.containsField(oldName)) {
|
|
infowindow.removeField(oldName);
|
|
infowindow.addField(newName);
|
|
}
|
|
}, infowindow);
|
|
this.bind('columnDelete', function(oldName, newName) {
|
|
infowindow.removeField(oldName);
|
|
}, infowindow);
|
|
|
|
this.bind('change:schema', function() {
|
|
var self = this;
|
|
var columns = _(this.columnNames()).filter(function(c) {
|
|
return !_.contains(infowindow.SYSTEM_COLUMNS, c);
|
|
});
|
|
|
|
function _hash(str){
|
|
var hash = 0, c;
|
|
for (i = 0; i < str.length; i++) {
|
|
c = str.charCodeAt(i);
|
|
hash = c + (hash << 6) + (hash << 16) - hash;
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
if (this.isInSQLView()) {
|
|
if (!infowindow.has('defaul_schema_fields')) {
|
|
infowindow.saveFields('defaul_schema_fields');
|
|
}
|
|
var current_schema_key = 'schema_' + _hash(self.columnNames().sort().join(''));
|
|
var previous_schema_key = 'schema_' + _hash(
|
|
_(self.previous('schema')).pluck(0).sort().join('')
|
|
);
|
|
|
|
if(!infowindow.has(previous_schema_key)) {
|
|
infowindow.saveFields(previous_schema_key);
|
|
}
|
|
if(infowindow.has(current_schema_key)) {
|
|
infowindow.restoreFields(null, current_schema_key);
|
|
}
|
|
} else {
|
|
infowindow.restoreFields(null, 'defaul_schema_fields');
|
|
}
|
|
|
|
if (infowindow.get('template')) {
|
|
// merge fields checking actual schema
|
|
infowindow.mergeFields(columns);
|
|
} else {
|
|
// remove fields that no longer exist
|
|
infowindow.removeMissingFields(columns);
|
|
}
|
|
}, this);
|
|
|
|
},
|
|
|
|
embedURL: function() {
|
|
return "/tables/" + this.get('name') + "/embed_map"
|
|
},
|
|
|
|
/**
|
|
* @deprecated use vis.viewUrl() or vis.viewUrl(currentUser) instead.
|
|
*/
|
|
viewUrl: function() {
|
|
return cdb.config.prefixUrl() + "/tables/" + this.getUnqualifiedName()
|
|
},
|
|
|
|
hasTheGeom: function() {
|
|
var currentSchema = this.get('schema');
|
|
// if we have "the_geom" in our current schema, returnstrue
|
|
for(var n in currentSchema) {
|
|
if(currentSchema[n][0] === 'the_geom') {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Checks the server to see if the table has any georeferenced row, independently of the applyed query
|
|
* @return {promise}
|
|
*/
|
|
fetchGeoreferenceStatus: function() {
|
|
var dfd = $.Deferred();
|
|
var username = (this.options && this.options.user_data)? this.options.user_data.username :
|
|
(window.user_data? window.user_data.username : window.user_name);
|
|
var api_key = (this.options && this.options.user_data)? this.options.user_data.api_key :
|
|
(window.user_data? window.user_data.api_key : window.api_key);
|
|
|
|
|
|
this.sqlApi = new this.sqlApiClass({
|
|
user: username,
|
|
version: 'v1',
|
|
api_key: api_key,
|
|
sql_api_template: cdb.config.getSqlApiBaseUrl()
|
|
});
|
|
|
|
var sql = 'SELECT the_geom FROM ' + this.get('name') + ' WHERE the_geom is not null';
|
|
this.sqlApi.execute(sql).done(function(data){
|
|
if(data.rows.length > 0) {
|
|
dfd.resolve(true);
|
|
} else {
|
|
dfd.resolve(false);
|
|
}
|
|
});
|
|
|
|
return dfd.promise();
|
|
|
|
},
|
|
|
|
/**
|
|
* Checks the current loaded records to see if they are georeferenced
|
|
* @return {boolean}
|
|
*/
|
|
isGeoreferenced: function() {
|
|
var geoColumns = this.geomColumnTypes();
|
|
if(geoColumns && geoColumns.length > 0) {
|
|
return true;
|
|
} else {
|
|
if (!this.isInSQLView()) {
|
|
// sometimes the columns are changed in the frontend site
|
|
// and the geocolumns are not updated.
|
|
// check the columns in local
|
|
return this._data.any(function(row) {
|
|
return row.hasGeometry();
|
|
});
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* this function can only be called during change event
|
|
* returns true if the geometry type has changed
|
|
* for this method multipolygon and polygon are the same geometry type
|
|
*/
|
|
geometryTypeChanged: function() {
|
|
if (!('geometry_types' in this.changed)) return false;
|
|
var geoTypes = this.get('geometry_types')
|
|
var prevGeoTypes = this.previousAttributes().geometry_types;
|
|
function normalize(e) {
|
|
e = e.toLowerCase();
|
|
if (e === 'st_multipolygon') {
|
|
return 'st_polygon'
|
|
}
|
|
if (e === 'st_multilinestring') {
|
|
return 'st_linestring'
|
|
}
|
|
if (e === 'st_multipoint') {
|
|
return 'st_point'
|
|
}
|
|
return e;
|
|
}
|
|
|
|
if(!geoTypes ||
|
|
geoTypes.length === 0 ||
|
|
!prevGeoTypes ||
|
|
prevGeoTypes.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
var n = normalize(geoTypes[0]);
|
|
var o = normalize(prevGeoTypes[0]);
|
|
return n !== o;
|
|
},
|
|
|
|
/**
|
|
* Get necessary data create a duplicated dataset from this table.
|
|
*
|
|
* @param {Object} newName name of new dataset.
|
|
* @param {Object} callbacks
|
|
* @returns {Object}
|
|
*/
|
|
duplicate: function(newName, callbacks) {
|
|
callbacks = callbacks || {};
|
|
|
|
// Extracted from duplicate_table_dialog
|
|
var data = {
|
|
table_name: newName
|
|
};
|
|
|
|
// Set correct data object, depending on if the app has a query applied or not
|
|
if (this.isInSQLView()) {
|
|
var query = this.data().getSQL();
|
|
data.sql = ( !query || query == "" ) ? 'SELECT * FROM ' + cdb.Utils.safeTableNameQuoting(this.get('name')) : query;
|
|
} else {
|
|
data.table_copy = this.get('name');
|
|
}
|
|
|
|
var importModel = new cdb.admin.Import();
|
|
importModel.save(data, {
|
|
error: callbacks.error,
|
|
success: function(model, changes) {
|
|
var checkImportModel = new cdb.admin.Import({
|
|
item_queue_id: changes.item_queue_id
|
|
});
|
|
|
|
checkImportModel.bind('importComplete', function() {
|
|
checkImportModel.unbind();
|
|
|
|
// So import is done, create new table object from the new table and fetch, callback once finished.
|
|
var newTable = new cdb.admin.CartoDBTableMetadata({
|
|
id: checkImportModel.get('table_id')
|
|
});
|
|
|
|
newTable.fetch({
|
|
success: function() {
|
|
callbacks.success(newTable);
|
|
},
|
|
error: callbacks.error
|
|
});
|
|
});
|
|
|
|
checkImportModel.bind('importError', function() {
|
|
checkImportModel.unbind();
|
|
callbacks.error.apply(this, arguments);
|
|
});
|
|
|
|
checkImportModel.pollCheck();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Get the visualizations that are using this table dataset.
|
|
* Note! a .fetch() is required to be sure the data to be available.
|
|
* @return {Array}
|
|
*/
|
|
dependentVisualizations: function() {
|
|
// dependent = visualizations with a single layer
|
|
// non-dependant = have more than this dataset as a layer
|
|
return _.chain(this.get('dependent_visualizations'))
|
|
.union(this.get('non_dependent_visualizations'))
|
|
.compact()
|
|
.value() || [];
|
|
}
|
|
|
|
}, {
|
|
/**
|
|
* creates a new table from query
|
|
* the called is responsable of calling save to create
|
|
* the table in the server
|
|
*/
|
|
createFromQuery: function(name, query) {
|
|
return new cdb.admin.CartoDBTableMetadata({
|
|
sql: query,
|
|
name: name
|
|
});
|
|
},
|
|
|
|
sortSchema: function(schema) {
|
|
var priorities = {
|
|
'cartodb_id': 1,
|
|
'the_geom': 2,
|
|
'__default__': 3,
|
|
'created_at': 4,
|
|
'updated_at': 5
|
|
};
|
|
|
|
function priority(v) {
|
|
return priorities[v] || priorities['__default__'];
|
|
}
|
|
|
|
return _.chain(schema)
|
|
.clone()
|
|
.sort(function(a, b) { // ..and then re-sort by priorities defined above
|
|
var prioA = priority(a[0]);
|
|
var prioB = priority(b[0]);
|
|
if (prioA < prioB) {
|
|
return -1;
|
|
} else if (prioA > prioB) {
|
|
return 1;
|
|
} else { //priority is the same (i.e. __default__), so compare alphabetically
|
|
return a[0] < b[0] ? -1 : 1;
|
|
}
|
|
})
|
|
.value();
|
|
},
|
|
|
|
/**
|
|
* return true if the sql query alters table schema in some way
|
|
*/
|
|
alterTable: function(sql) {
|
|
sql = sql.trim();
|
|
return sql.search(/alter\s+[\w\."]+\s+/i) !== -1 ||
|
|
sql.search(/drop\s+[\w\.\"]+/i) !== -1 ||
|
|
sql.search(/^vacuum\s+[\w\.\"]+/i) !== -1 ||
|
|
sql.search(/^create\s+[\w\.\"]+/i) !== -1 ||
|
|
sql.search(/^reindex\s+[\w\.\"]+/i) !== -1 ||
|
|
sql.search(/^grant\s+[\w\.\"]+/i) !== -1 ||
|
|
sql.search(/^revoke\s+[\w\.\"]+/i) !== -1 ||
|
|
sql.search(/^cluster\s+[\w\.\"]+/i) !== -1 ||
|
|
sql.search(/^comment\s+on\s+[\w\.\"]+/i) !== -1 ||
|
|
sql.search(/^explain\s+[\w\.\"]+/i) !== -1;
|
|
},
|
|
|
|
/**
|
|
* return true if the sql query alters table data
|
|
*/
|
|
alterTableData: function(sql) {
|
|
return this.alterTable(sql) ||
|
|
sql.search(/^refresh\s+materialized\s+view\s+[\w\.\"]+/i) !== -1 ||
|
|
sql.search(/^truncate\s+[\w\.\"]+/i) !== -1 ||
|
|
sql.search(/insert\s+into/i) !== -1 ||
|
|
sql.search(/update\s+[\w\.\-"]+\s+.*set/i) !== -1 ||
|
|
sql.search(/delete\s+from/i) !== -1;
|
|
}
|
|
|
|
});
|
|
|
|
cdb.admin.Row = cdb.core.Model.extend({
|
|
|
|
_CREATED_EVENT: 'created',
|
|
_CREATED_EVENT: 'creating',
|
|
sqlApiClass: cartodb.SQL,
|
|
|
|
_GEOMETRY_TYPES: {
|
|
'point': 'st_point',
|
|
'multipoint': 'st_multipoint',
|
|
'linestring': 'st_linestring',
|
|
'multilinestring': 'st_multilinestring',
|
|
'polygon': 'st_polygon',
|
|
'multipolygon': 'st_multipolygon'
|
|
},
|
|
|
|
url: function(method) {
|
|
var version = cdb.config.urlVersion('record', method);
|
|
var table = this.table || this.collection.table;
|
|
if(!table) {
|
|
cdb.log.error("row has no table assigned");
|
|
}
|
|
|
|
var base = '/api/' + version + '/tables/' + table.get('name') + '/records/';
|
|
if (this.isNew()) {
|
|
return base;
|
|
}
|
|
return base + this.id;
|
|
},
|
|
|
|
fetch: function(opts) {
|
|
opts = opts || {}
|
|
var self = this;
|
|
var silent = opts && opts.silent;
|
|
var username = (this.options && this.options.user_data)? this.options.user_data.username :
|
|
(window.user_data? window.user_data.username : window.user_name);
|
|
var api_key = (this.options && this.options.user_data)? this.options.user_data.api_key :
|
|
(window.user_data? window.user_data.api_key : window.api_key);
|
|
|
|
var table = this.table || this.collection.table;
|
|
|
|
var sqlApi = new this.sqlApiClass({
|
|
user: username,
|
|
version: 'v2',
|
|
api_key: api_key,
|
|
sql_api_template: cdb.config.getSqlApiBaseUrl(),
|
|
extra_params: ['skipfields']
|
|
});
|
|
// this.trigger('loading')
|
|
var sql = null;
|
|
var columns = table.columnNames()
|
|
if (opts.no_geom) {
|
|
columns = _.without(columns, 'the_geom', 'the_geom_webmercator');
|
|
} else {
|
|
columns = _.without(columns, 'the_geom');
|
|
}
|
|
sql = 'SELECT ' + columns.join(',') + " "
|
|
if(table.containsColumn('the_geom') && !opts.no_geom) {
|
|
sql += ',ST_AsGeoJSON(the_geom, 8) as the_geom '
|
|
}
|
|
sql += ' from (' + table.data().getSQL() + ') _table_sql WHERE cartodb_id = ' + this.get('cartodb_id');
|
|
// Added opts to sql execute function to apply
|
|
// parameters ( like cache ) to the ajax request
|
|
if (opts.no_geom) {
|
|
opts.skipfields = 'the_geom,the_geom_webmercator';
|
|
} else {
|
|
opts.skipfields = 'the_geom_webmercator';
|
|
}
|
|
sqlApi.execute(sql, {}, opts).done(function(data){
|
|
if(self.parse(data.rows[0])) {
|
|
self.set(data.rows[0]);//, {silent: silent});
|
|
self.trigger('sync');
|
|
}
|
|
});
|
|
|
|
},
|
|
|
|
toJSON: function() {
|
|
var attr = _.clone(this.attributes);
|
|
// remove read-only attributes
|
|
delete attr['updated_at'];
|
|
delete attr['created_at'];
|
|
delete attr['the_geom_webmercator'];
|
|
if(!this.isGeometryGeoJSON()) {
|
|
delete attr['the_geom'];
|
|
}
|
|
return attr;
|
|
},
|
|
|
|
isGeomLoaded: function() {
|
|
var geojson = this.get('the_geom');
|
|
var column_types_WKT = cdb.admin.WKT.types
|
|
return (geojson !== 'GeoJSON' && geojson !== -1 && !_.contains(column_types_WKT, geojson));
|
|
},
|
|
|
|
hasGeometry: function() {
|
|
var the_geom = this.get('the_geom');
|
|
return !!(the_geom != null && the_geom != undefined && the_geom != '')
|
|
// in fact, if the_geom has anything but null or '', the row is georeferenced
|
|
|
|
// if(typeof the_geom === 'string') {
|
|
// // if the geom contains GeoJSON, the row has a valid geometry, but is not loaded yet
|
|
// if(the_geom === 'GeoJSON') {
|
|
// return true
|
|
// }
|
|
|
|
// try {
|
|
// var g = JSON.parse(the_geom);
|
|
// return !!g.coordinates;
|
|
// } catch(e) {
|
|
// return false;
|
|
// }
|
|
// } else {
|
|
// if(the_geom) {
|
|
// return !!the_geom.coordinates;
|
|
// }
|
|
// return false;
|
|
// }
|
|
},
|
|
/**
|
|
* Checks if the_geom contains a valid geoJson
|
|
*/
|
|
isGeometryGeoJSON: function() {
|
|
var the_geom = this.get('the_geom');
|
|
if(the_geom && typeof the_geom === 'object') {
|
|
return !!the_geom.coordinates;
|
|
} else if(typeof the_geom !== 'string') {
|
|
return false;
|
|
}
|
|
// if the geom contains GeoJSON, the row has a valid geometry, but is not loaded yet
|
|
if(the_geom === 'GeoJSON') {
|
|
return true
|
|
}
|
|
|
|
try {
|
|
var g = JSON.parse(the_geom);
|
|
return !!g.coordinates;
|
|
} catch(e) {
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
getFeatureType: function() {
|
|
if (this.isGeomLoaded()) {
|
|
// Problem geometry type from a WKB format
|
|
// Not possible for the moment
|
|
try {
|
|
var geojson = JSON.parse(this.get('the_geom'));
|
|
return geojson.type.toLowerCase();
|
|
} catch(e) {
|
|
cdb.log.info("Not possible to parse geometry type");
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
getGeomType: function() {
|
|
try {
|
|
return this._GEOMETRY_TYPES[this.getFeatureType()];
|
|
} catch(e) {
|
|
cdb.log.info("Not possible to parse geometry type");
|
|
}
|
|
}
|
|
|
|
}, {
|
|
RESERVED_COLUMNS: 'the_geom the_geom_webmercator cartodb_id created_at updated_at'.split(' '),
|
|
isReservedColumn: function(c) {
|
|
return _(cdb.admin.Row.RESERVED_COLUMNS).indexOf(c) !== -1;
|
|
}
|
|
});
|
|
|
|
})();
|