Initial commit
This commit is contained in:
167
lib/assets/javascripts/dashboard/data/api-key-model.js
Normal file
167
lib/assets/javascripts/dashboard/data/api-key-model.js
Normal file
@@ -0,0 +1,167 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel'
|
||||
];
|
||||
|
||||
const GRANT_TYPES = {
|
||||
APIS: 'apis',
|
||||
DATABASE: 'database'
|
||||
};
|
||||
|
||||
const TYPES = {
|
||||
MASTER: 'master',
|
||||
DEFAULT: 'default',
|
||||
REGULAR: 'regular'
|
||||
};
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
name: '',
|
||||
token: '',
|
||||
apis: {
|
||||
maps: false,
|
||||
sql: false
|
||||
},
|
||||
datasets: {
|
||||
create: false,
|
||||
listing: false
|
||||
},
|
||||
tables: []
|
||||
},
|
||||
|
||||
initialize: function (attributes, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
regenerate: function () {
|
||||
const options = {
|
||||
url: `${this.url()}/token/regenerate`,
|
||||
type: 'POST',
|
||||
success: (data) => this.set(data)
|
||||
};
|
||||
|
||||
return this.sync(null, this, options);
|
||||
},
|
||||
|
||||
parse: function (data, options) {
|
||||
const schemaName = options.userModel.getSchema();
|
||||
const { grants, ...attrs } = data;
|
||||
const apis = this._parseApiGrants(grants);
|
||||
const tables = this._parseTableGrants(grants);
|
||||
|
||||
const datasets = {
|
||||
create: this._parseDatabaseSchemas(grants, schemaName),
|
||||
listing: this._parseDatabaseGrants(grants)
|
||||
};
|
||||
|
||||
return {
|
||||
...attrs,
|
||||
apis,
|
||||
tables,
|
||||
datasets,
|
||||
id: attrs.name
|
||||
};
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
// Extract apis and tables properties to not include in JSON
|
||||
const { apis, tables, datasets, ...attrs } = this.attributes;
|
||||
|
||||
const grants = [
|
||||
{ type: GRANT_TYPES.APIS, apis: this.getApiGrants() },
|
||||
{
|
||||
type: GRANT_TYPES.DATABASE,
|
||||
...this.getDatabaseGrants()
|
||||
}
|
||||
];
|
||||
|
||||
return { ...attrs, grants };
|
||||
},
|
||||
|
||||
isPublic: function () {
|
||||
return this.get('type') === TYPES.DEFAULT;
|
||||
},
|
||||
|
||||
getApiGrants: function () {
|
||||
const apis = this.get('apis');
|
||||
return Object.keys(apis).filter(name => apis[name]);
|
||||
},
|
||||
|
||||
getDatabaseGrants: function () {
|
||||
const grants = {
|
||||
tables: this.getTablesGrants(),
|
||||
schemas: this.getDatabaseSchemas()
|
||||
};
|
||||
|
||||
if (this.get('datasets').listing) {
|
||||
grants.table_metadata = [];
|
||||
}
|
||||
|
||||
return grants;
|
||||
},
|
||||
|
||||
getDatabaseSchemas: function () {
|
||||
const schemas = [];
|
||||
|
||||
if (this.get('datasets').create) {
|
||||
schemas.push({
|
||||
name: this._userModel.getSchema(),
|
||||
permissions: ['create']
|
||||
});
|
||||
}
|
||||
|
||||
return schemas;
|
||||
},
|
||||
|
||||
getTablesGrants: function () {
|
||||
const tables = _.map(this.get('tables'), (table, tableName) => ({
|
||||
name: tableName,
|
||||
schema: this._userModel.getSchema(),
|
||||
permissions: Object.keys(table.permissions).filter(name => table.permissions[name])
|
||||
}));
|
||||
|
||||
return _.filter(tables, table => table.permissions.length > 0);
|
||||
},
|
||||
|
||||
_parseApiGrants: function (grants) {
|
||||
const apis = _.find(grants, grant => grant.type === GRANT_TYPES.APIS).apis;
|
||||
const apisObj = this._arrayToObj(apis);
|
||||
|
||||
return { ...this.defaults.apis, ...apisObj };
|
||||
},
|
||||
|
||||
_parseTableGrants: function (grants) {
|
||||
const tables = _.find(grants, grant => grant.type === GRANT_TYPES.DATABASE).tables;
|
||||
const tablesObj = tables.reduce((total, table) => {
|
||||
const permissions = this._arrayToObj(table.permissions);
|
||||
return { ...total, [table.name]: { ...table, permissions } };
|
||||
}, {});
|
||||
|
||||
return tablesObj;
|
||||
},
|
||||
|
||||
_parseDatabaseGrants: function (grants) {
|
||||
return !!_.find(grants, grant => grant.type === GRANT_TYPES.DATABASE).table_metadata;
|
||||
},
|
||||
|
||||
_parseDatabaseSchemas: function (grants, schemaName) {
|
||||
const schemas = _.find(grants, grant => grant.type === GRANT_TYPES.DATABASE).schemas;
|
||||
const schema = _.find(schemas, schema => schema.name === schemaName);
|
||||
return !!(schema && schema.permissions && schema.permissions.indexOf('create') > -1);
|
||||
},
|
||||
|
||||
_arrayToObj: function (arr) {
|
||||
return arr.reduce((total, item) => ({ ...total, [item]: true }), {});
|
||||
},
|
||||
|
||||
hasPermissionsSelected: function () {
|
||||
return _.some(this.getTablesGrants().map(table => !_.isEmpty(table.permissions)));
|
||||
},
|
||||
|
||||
urlRoot: function () {
|
||||
return `${this._userModel.get('base_url')}/api/v3/api_keys`;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
MASTER: 'master',
|
||||
DEFAULT: 'default',
|
||||
REGULAR: 'regular'
|
||||
};
|
||||
84
lib/assets/javascripts/dashboard/data/api-keys-collection.js
Normal file
84
lib/assets/javascripts/dashboard/data/api-keys-collection.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const $ = require('jquery');
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const ApiKeyModel = require('dashboard/data/api-key-model');
|
||||
const apiKeysCollectionTypes = require('dashboard/data/api-keys-collection-types');
|
||||
const PaginationModel = require('builder/components/pagination/pagination-model');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel'
|
||||
];
|
||||
|
||||
const STATUS = {
|
||||
fetching: 'fetching',
|
||||
fetched: 'fetched',
|
||||
errored: 'errored'
|
||||
};
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
defaults: {
|
||||
status: STATUS.fetching
|
||||
},
|
||||
|
||||
initialize: function (models, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._type = options.type || [apiKeysCollectionTypes.REGULAR];
|
||||
this._paginationModel = new PaginationModel({
|
||||
per_page: 5,
|
||||
current_page: 1
|
||||
});
|
||||
|
||||
this.on('sync', this._onCollectionSync);
|
||||
this.on('error', () => { this.status = STATUS.errored; });
|
||||
this.listenTo(this._paginationModel, 'change:current_page', this.fetch);
|
||||
},
|
||||
|
||||
url: function () {
|
||||
const urlParams = {
|
||||
per_page: this._paginationModel.get('per_page'),
|
||||
page: this._paginationModel.get('current_page'),
|
||||
type: this._type
|
||||
};
|
||||
|
||||
return `${this._userModel.get('base_url')}/api/v3/api_keys?${$.param(urlParams)}`;
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
const options = {
|
||||
headers: {
|
||||
'Authorization': `Basic ${this._userModel.getAuthToken()}`
|
||||
}
|
||||
};
|
||||
|
||||
Backbone.Collection.prototype.fetch.call(this, options);
|
||||
},
|
||||
|
||||
model: function (attrs, opts) {
|
||||
const options = { ...opts, userModel: opts.collection._userModel };
|
||||
|
||||
return new ApiKeyModel(attrs, options);
|
||||
},
|
||||
|
||||
parse: function ({ result, ...stats }) {
|
||||
this._stats = stats;
|
||||
|
||||
return result.map(key => ({ ...key, id: key.name })); // We are using the name as an unique id
|
||||
},
|
||||
|
||||
_onCollectionSync: function () {
|
||||
this.status = STATUS.fetched;
|
||||
|
||||
this._paginationModel.set({
|
||||
total_count: this._getTotalPages()
|
||||
});
|
||||
},
|
||||
|
||||
_getTotalPages: function (attribute) {
|
||||
return (this._stats && this._stats.total) || 0;
|
||||
},
|
||||
|
||||
getPaginationModel: function () {
|
||||
return this._paginationModel;
|
||||
}
|
||||
});
|
||||
27
lib/assets/javascripts/dashboard/data/asset-model.js
Normal file
27
lib/assets/javascripts/dashboard/data/asset-model.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
/**
|
||||
* Model that let user upload files
|
||||
* to our endpoints
|
||||
*/
|
||||
|
||||
require('backbone-model-file-upload');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'userId'
|
||||
];
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('asset', method);
|
||||
return `/api/${version}/users/${this._userId}/assets`;
|
||||
},
|
||||
|
||||
fileAttribute: 'filename',
|
||||
|
||||
initialize: function (attributes, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
username: '',
|
||||
avatar_url: ''
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return `//${this.getHost()}/api/v1/get_authenticated_users`;
|
||||
},
|
||||
|
||||
getHost: function () {
|
||||
var currentHost = window.location.host;
|
||||
return this.get('host') ? this.get('host') : currentHost;
|
||||
}
|
||||
});
|
||||
23
lib/assets/javascripts/dashboard/data/backbone/sync-abort.js
Normal file
23
lib/assets/javascripts/dashboard/data/backbone/sync-abort.js
Normal file
@@ -0,0 +1,23 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Custom sync method to only allow a single request at a time,
|
||||
* any prev ongoing request at a time of a sync call will be aborted.
|
||||
*
|
||||
* @example
|
||||
* var MyModel = Backbone.Model.extend({
|
||||
* // …
|
||||
* sync: syncAbort,
|
||||
*/
|
||||
module.exports = function (method, self, opts) {
|
||||
if (this._xhr) {
|
||||
this._xhr.abort();
|
||||
}
|
||||
|
||||
var xhr = this._xhr = Backbone.Model.prototype.sync.apply(this, arguments);
|
||||
xhr.always(function () {
|
||||
self._xhr = null;
|
||||
});
|
||||
|
||||
return xhr;
|
||||
};
|
||||
217
lib/assets/javascripts/dashboard/data/backbone/sync-options.js
Normal file
217
lib/assets/javascripts/dashboard/data/backbone/sync-options.js
Normal file
@@ -0,0 +1,217 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
|
||||
(function () {
|
||||
// helper functions needed from backbone (they are not exported)
|
||||
var getValue = function (object, prop, method) {
|
||||
if (!(object && object[prop])) return null;
|
||||
return _.isFunction(object[prop]) ? object[prop](method) : object[prop];
|
||||
};
|
||||
|
||||
// Throw an error when a URL is needed, and none is supplied.
|
||||
var urlError = function () {
|
||||
throw new Error('A "url" property or function must be specified');
|
||||
};
|
||||
|
||||
// backbone.sync replacement to control url prefix
|
||||
Backbone.originalSync = Backbone.sync;
|
||||
Backbone.sync = function (method, model, options) {
|
||||
var url = options.url || getValue(model, 'url', method) || urlError();
|
||||
// prefix if http is not present
|
||||
var absoluteUrl = url.indexOf('http') === 0 || url.indexOf('//') === 0;
|
||||
if (!absoluteUrl) {
|
||||
// We need to fix this
|
||||
// this comes from cdb.config.prefixUrl
|
||||
options.url = (model._configModel || model._config || model).get('base_url') + url;
|
||||
} else {
|
||||
options.url = url;
|
||||
}
|
||||
if (method !== 'read') {
|
||||
// remove everything related
|
||||
if (model.surrogateKeys) {
|
||||
Backbone.cachedSync.invalidateSurrogateKeys(getValue(model, 'surrogateKeys'));
|
||||
}
|
||||
}
|
||||
return Backbone.originalSync(method, model, options);
|
||||
};
|
||||
|
||||
Backbone.currentSync = Backbone.sync;
|
||||
Backbone.withCORS = function (method, model, options) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (!options.crossDomain) {
|
||||
options.crossDomain = true;
|
||||
}
|
||||
|
||||
if (!options.xhrFields) {
|
||||
options.xhrFields = { withCredentials: true };
|
||||
}
|
||||
|
||||
return Backbone.currentSync(method, model, options);
|
||||
};
|
||||
|
||||
// this method returns a cached version of backbone sync
|
||||
// take a look at https://github.com/teambox/backbone.memoized_sync/blob/master/backbone.memoized_sync.js
|
||||
// this is the same concept but implemented as a wrapper for ``Backbone.sync``
|
||||
// usage:
|
||||
// initialize: function () {
|
||||
// this.sync = Backbone.cachedSync(this.user_name);
|
||||
// }
|
||||
Backbone.cachedSync = function (namespace, sync) {
|
||||
if (!namespace) {
|
||||
throw new Error('cachedSync needs a namespace as argument');
|
||||
}
|
||||
|
||||
var surrogateKey = namespace;
|
||||
var session = window.user_data && window.user_data.username;
|
||||
// no user session, no cache
|
||||
// there should be a session to have cache so we avoid
|
||||
// cache collision for someone with more than one account
|
||||
if (session) {
|
||||
namespace += '-' + session;
|
||||
} else {
|
||||
return Backbone.sync;
|
||||
}
|
||||
|
||||
var namespaceKey = 'cdb-cache/' + namespace;
|
||||
|
||||
// saves all the localstore references to the namespace
|
||||
// inside localstore. It allows to remove all the references
|
||||
// at a time
|
||||
var index = {
|
||||
// return a list of references for the namespace
|
||||
_keys: function () {
|
||||
return JSON.parse(localStorage.getItem(namespaceKey) || '{}');
|
||||
},
|
||||
|
||||
// add a new reference for the namespace
|
||||
add: function (key) {
|
||||
var keys = this._keys();
|
||||
keys[key] = +new Date();
|
||||
localStorage.setItem(namespaceKey, JSON.stringify(keys));
|
||||
},
|
||||
|
||||
// remove all the references for the namespace
|
||||
invalidate: function () {
|
||||
var keys = this._keys();
|
||||
_.each(keys, function (v, k) {
|
||||
localStorage.removeItem(k);
|
||||
});
|
||||
localStorage.removeItem(namespaceKey);
|
||||
}
|
||||
};
|
||||
|
||||
// localstore-like cache wrapper
|
||||
var cache = {
|
||||
setItem: function (key, value) {
|
||||
localStorage.setItem(key, value);
|
||||
index.add(key);
|
||||
return this;
|
||||
},
|
||||
|
||||
// this is async in case the data needs to be compressed
|
||||
getItem: function (key, callback) {
|
||||
var val = localStorage.getItem(key);
|
||||
_.defer(function () {
|
||||
callback(val);
|
||||
});
|
||||
},
|
||||
|
||||
removeItem: function (key) {
|
||||
localStorage.removeItem(key);
|
||||
index.invalidate();
|
||||
}
|
||||
};
|
||||
|
||||
var cached = function (method, model, options) {
|
||||
var url = options.url || getValue(model, 'url') || urlError();
|
||||
var key = namespaceKey + '/' + url;
|
||||
|
||||
if (method === 'read') {
|
||||
var success = options.success;
|
||||
var cachedValue = null;
|
||||
|
||||
options.success = function (resp, status, xhr) {
|
||||
// if cached value is ok
|
||||
if (cachedValue && xhr.responseText === cachedValue) {
|
||||
return;
|
||||
}
|
||||
cache.setItem(key, xhr.responseText);
|
||||
success(resp, status, xhr);
|
||||
};
|
||||
|
||||
cache.getItem(key, function (val) {
|
||||
cachedValue = val;
|
||||
if (val) {
|
||||
success(JSON.parse(val), 'success');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cache.removeItem(key);
|
||||
}
|
||||
return (sync || Backbone.sync)(method, model, options);
|
||||
};
|
||||
|
||||
// create a public function to invalidate all the namespace
|
||||
// items
|
||||
cached.invalidate = function () {
|
||||
index.invalidate();
|
||||
};
|
||||
|
||||
// for testing and debugging porpuposes
|
||||
cached.cache = cache;
|
||||
|
||||
// have a global namespace -> sync function in order to avoid invalidation
|
||||
Backbone.cachedSync.surrogateKeys[surrogateKey] = cached;
|
||||
|
||||
return cached;
|
||||
};
|
||||
|
||||
Backbone.cachedSync.surrogateKeys = {};
|
||||
|
||||
Backbone.cachedSync.invalidateSurrogateKeys = function (keys) {
|
||||
_.each(keys, function (k) {
|
||||
var s = Backbone.cachedSync.surrogateKeys[k];
|
||||
if (s) {
|
||||
s.invalidate();
|
||||
} else {
|
||||
console.error('Backbone sync options: surrogate key not found: ' + k);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Backbone.syncAbort = function () {
|
||||
var self = arguments[1];
|
||||
if (self._xhr) {
|
||||
self._xhr.abort();
|
||||
}
|
||||
self._xhr = Backbone.sync.apply(this, arguments);
|
||||
self._xhr.always(function () { self._xhr = null; });
|
||||
return self._xhr;
|
||||
};
|
||||
|
||||
Backbone.delayedSaveSync = function (sync, delay) {
|
||||
var dsync = _.debounce(sync, delay);
|
||||
return function (method, model, options) {
|
||||
if (method === 'create' || method === 'update') {
|
||||
return dsync(method, model, options);
|
||||
} else {
|
||||
return sync(method, model, options);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Backbone.saveAbort = function () {
|
||||
var self = this;
|
||||
if (this._saving && this._xhr) {
|
||||
this._xhr.abort();
|
||||
}
|
||||
this._saving = true;
|
||||
var xhr = Backbone.Model.prototype.save.apply(this, arguments);
|
||||
this._xhr = xhr;
|
||||
xhr.always(function () { self._saving = false; });
|
||||
return xhr;
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,104 @@
|
||||
const Backbone = require('backbone');
|
||||
const ImportsCollection = require('dashboard/data/imports-collection');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const pollingsTimer = 3000;
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Background polling default model
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
showGeocodingDatasetURLButton: false,
|
||||
showSuccessDetailsButton: true,
|
||||
geocodingsPolling: false, // enable geocodings polling
|
||||
importsPolling: false // enable imports polling
|
||||
},
|
||||
|
||||
initialize: function (attributes, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.importsCollection = options.importsCollection || new ImportsCollection(null, {
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
this._initBinds();
|
||||
this.startPollings();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.importsCollection.bind('change:state', function (mdl) {
|
||||
this.trigger('change', mdl, this);
|
||||
this._onImportsStateChange(mdl);
|
||||
}, this);
|
||||
this.importsCollection.bind('remove', function (mdl) {
|
||||
this.trigger('importRemoved', mdl, this);
|
||||
}, this);
|
||||
this.importsCollection.bind('add', function (mdl) {
|
||||
this.trigger('importAdded', mdl, this);
|
||||
}, this);
|
||||
},
|
||||
|
||||
// Helper functions
|
||||
|
||||
getTotalFailedItems: function () {
|
||||
return this.importsCollection.failedItems().length;
|
||||
},
|
||||
|
||||
removeImportItem: function (mdl) {
|
||||
if (!mdl) {
|
||||
return false;
|
||||
}
|
||||
this.importsCollection.remove(mdl);
|
||||
},
|
||||
|
||||
addImportItem: function (mdl) {
|
||||
if (!mdl) {
|
||||
return false;
|
||||
}
|
||||
this.importsCollection.add(mdl);
|
||||
},
|
||||
|
||||
canAddImport: function () {
|
||||
return this.importsCollection.canImport();
|
||||
},
|
||||
|
||||
getTotalImports: function () {
|
||||
return this.importsCollection.size();
|
||||
},
|
||||
|
||||
getTotalPollings: function () {
|
||||
return this.importsCollection.size();
|
||||
},
|
||||
|
||||
stopPollings: function () {
|
||||
if (this.get('importsPolling')) {
|
||||
this.importsCollection.destroyCheck();
|
||||
}
|
||||
},
|
||||
|
||||
startPollings: function () {
|
||||
// Don't start pollings inmediately,
|
||||
// wait some seconds
|
||||
setTimeout(() => {
|
||||
if (this.get('importsPolling')) {
|
||||
this.importsCollection.pollCheck();
|
||||
}
|
||||
}, pollingsTimer);
|
||||
},
|
||||
|
||||
// onChange functions
|
||||
_onImportsStateChange: function () {},
|
||||
|
||||
clean: function () {
|
||||
this.importsCollection.unbind(null, null, this);
|
||||
Backbone.Model.prototype.clean.apply(this);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
const BackgroundPollingModel = require('dashboard/data/background-polling/background-polling-model');
|
||||
|
||||
/**
|
||||
* Background polling model for the dashboard context.
|
||||
*/
|
||||
module.exports = BackgroundPollingModel.extend({
|
||||
|
||||
_onImportsStateChange: function (importsModel) {
|
||||
if (this._shouldRedirect(importsModel)) {
|
||||
this._redirectTo(importsModel.getRedirectUrl(this._userModel));
|
||||
return;
|
||||
}
|
||||
|
||||
if (importsModel.hasCompleted()) {
|
||||
this.trigger('importCompleted', importsModel, this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the user can be redirected after the given
|
||||
* import has been completed. The following conditions must be true:
|
||||
* - Import has been completed
|
||||
* - No imports are still running
|
||||
* - No other imports have been previously completed
|
||||
* - Import has a redirect URL for the given user
|
||||
* - Import was NOT a twitter import
|
||||
* - Number of tables created:
|
||||
* - n if import was a .carto file
|
||||
* - 1 otherwise
|
||||
*/
|
||||
_shouldRedirect: function (importsModel) {
|
||||
return importsModel.hasCompleted() &&
|
||||
this.importsCollection.allImportsCompletedOrFailed() &&
|
||||
this.importsCollection.getCompletedItemsCount() === 1 &&
|
||||
!importsModel.isTwitterImport() &&
|
||||
importsModel.getRedirectUrl(this._userModel) &&
|
||||
(importsModel.getNumberOfTablesCreated() === 1 ||
|
||||
importsModel.isCartoImport());
|
||||
},
|
||||
|
||||
_redirectTo: function (url) {
|
||||
window.location = url;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
const _ = require('underscore');
|
||||
const Poller = require('./poller');
|
||||
|
||||
const GeocodingModelPoller = function (model) {
|
||||
const POLLING_INTERVAL = 2000;
|
||||
|
||||
const options = {
|
||||
interval: POLLING_INTERVAL,
|
||||
stopWhen: function (model) {
|
||||
return model.hasFailed() || model.hasCompleted();
|
||||
},
|
||||
error: function (model) {
|
||||
model.trigger('change');
|
||||
}
|
||||
};
|
||||
|
||||
Poller.call(this, model, options);
|
||||
};
|
||||
|
||||
GeocodingModelPoller.prototype = _.extend({}, Poller.prototype);
|
||||
|
||||
module.exports = GeocodingModelPoller;
|
||||
@@ -0,0 +1,116 @@
|
||||
const Backbone = require('backbone');
|
||||
const _ = require('underscore');
|
||||
const GeocodingModelPoller = require('./geocoding-model-poller');
|
||||
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Geocoding model
|
||||
*
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
options: {
|
||||
startPollingAutomatically: true
|
||||
},
|
||||
|
||||
defaults: {
|
||||
kind: '',
|
||||
formatter: '',
|
||||
table_name: '',
|
||||
state: ''
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._initBinds();
|
||||
// TODO: Check if we can remove this thing
|
||||
_.extend(this.options, options);
|
||||
|
||||
this.poller = new GeocodingModelPoller(this);
|
||||
|
||||
if (this.options.startPollingAutomatically) {
|
||||
this._checkModel();
|
||||
}
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('geocoding', method);
|
||||
|
||||
const base = `/api/${version}/geocodings/`;
|
||||
|
||||
if (this.isNew()) {
|
||||
return base;
|
||||
}
|
||||
return base + this.id;
|
||||
},
|
||||
|
||||
setUrlRoot: function (urlRoot) {
|
||||
this.urlRoot = urlRoot;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('change:id', this._checkModel, this);
|
||||
},
|
||||
|
||||
_checkModel: function () {
|
||||
if (this.get('id')) {
|
||||
this.pollCheck();
|
||||
} else {
|
||||
this._saveModel();
|
||||
}
|
||||
},
|
||||
|
||||
_saveModel: function () {
|
||||
if (this.isNew()) {
|
||||
this.save({}, {
|
||||
error: () => {
|
||||
this.set({
|
||||
state: 'failed',
|
||||
error: {
|
||||
title: 'Oops, there was a problem',
|
||||
description: 'Unfortunately there was an error starting the geocoder'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
pollCheck: function () {
|
||||
this.poller.start();
|
||||
},
|
||||
|
||||
destroyCheck: function () {
|
||||
this.poller.stop();
|
||||
},
|
||||
|
||||
getError: function () {
|
||||
return this.get('error');
|
||||
},
|
||||
|
||||
hasFailed: function () {
|
||||
var state = this.get('state');
|
||||
return state === 'failed' || state === 'reset' || state === 'cancelled';
|
||||
},
|
||||
|
||||
hasCompleted: function () {
|
||||
return this.get('state') === 'finished';
|
||||
},
|
||||
|
||||
isOngoing: function () {
|
||||
return !this.hasCompleted() && !this.hasFailed();
|
||||
},
|
||||
|
||||
cancelGeocoding: function () {
|
||||
this.save({ state: 'cancelled' }, { wait: true });
|
||||
},
|
||||
|
||||
resetGeocoding: function () {
|
||||
this.set('state', 'reset');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const GeocodingModel = require('./geocoding-model');
|
||||
const pollTimer = 60000;
|
||||
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'userModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Geocodings collection
|
||||
*
|
||||
* - Check ongoing geocodings in order to add them
|
||||
* to the collection.
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
model: GeocodingModel,
|
||||
|
||||
url: function (method) {
|
||||
const version = this._configModel.urlVersion('geocoding', method);
|
||||
return `/api/${version}/geocodings`;
|
||||
},
|
||||
|
||||
initialize: function (models, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
parse: function (response) {
|
||||
_.each(response.geocodings, data => {
|
||||
// Check if that geocoding exists...
|
||||
const geocodings = this.filter(mdl => mdl.get('id') === data.id);
|
||||
|
||||
if (geocodings.length === 0) {
|
||||
this._checkOngoingGeocoding(
|
||||
new GeocodingModel(data, { startPollingAutomatically: false })
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return this.models;
|
||||
},
|
||||
|
||||
_checkOngoingGeocoding: function (model) {
|
||||
if (!this._visModel) {
|
||||
// If there is NOT a vis, let's start polling
|
||||
// this geocoding model
|
||||
this.add(model);
|
||||
model.pollCheck();
|
||||
} else {
|
||||
// If there is a vis, let's check if that
|
||||
// geocoding belongs to the visualization
|
||||
this.vis.map.layers.each(layer => {
|
||||
if (layer.table && layer.table.id === model.get('table_name')) {
|
||||
this.add(model);
|
||||
model.pollCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Public methods
|
||||
|
||||
canGeocode: function () {
|
||||
return !this.any(function (model) {
|
||||
return model.isOngoing();
|
||||
});
|
||||
},
|
||||
|
||||
fetchGeocodings: function () {
|
||||
this.fetch({
|
||||
error: e => this.destroyCheck()
|
||||
});
|
||||
},
|
||||
|
||||
pollCheck: function (i) {
|
||||
if (this.pollTimer) return;
|
||||
|
||||
this.pollTimer = setInterval(() => {
|
||||
this.fetchGeocodings();
|
||||
}, pollTimer);
|
||||
|
||||
this.fetchGeocodings();
|
||||
},
|
||||
|
||||
destroyCheck: function () {
|
||||
clearInterval(this.pollTimer);
|
||||
delete this.pollTimer;
|
||||
},
|
||||
|
||||
failedItems: function () {
|
||||
return this.filter(function (item) {
|
||||
return item.hasFailed();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
const _ = require('underscore');
|
||||
/*
|
||||
* Periodically fetches a model/collection. It waits for ongoing
|
||||
* fetch requests before trying to fetch again. A stop condition
|
||||
* can be specified.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* var poller = new Poller(model, {
|
||||
* interval: 1000,
|
||||
* stopWhen: function (model) {
|
||||
* return model.get('state') === 'completed';
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* poller.start();
|
||||
*
|
||||
* // ...
|
||||
*
|
||||
* poller.stop();
|
||||
*
|
||||
*/
|
||||
const Poller = function (model, options) {
|
||||
this.model = model;
|
||||
this.numberOfRequests = 0;
|
||||
this.polling = false;
|
||||
this.interval = options['interval'];
|
||||
|
||||
if (typeof this.interval !== 'function') {
|
||||
this.interval = function () { return options['interval']; };
|
||||
}
|
||||
|
||||
this.stopWhen = options['stopWhen'];
|
||||
this.error = options['error'];
|
||||
this.autoStart = options['autoStart'];
|
||||
|
||||
if (this.autoStart) {
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
|
||||
Poller.prototype.start = function () {
|
||||
if (this.timeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._scheduleFetch();
|
||||
};
|
||||
|
||||
Poller.prototype._scheduleFetch = function () {
|
||||
this.timeout = setTimeout(this._fetch.bind(this), this.interval(this.numberOfRequests));
|
||||
};
|
||||
|
||||
Poller.prototype._fetch = function () {
|
||||
if (!this.polling) {
|
||||
this.polling = true;
|
||||
this.model.fetch({
|
||||
success: () => {
|
||||
this.polling = false;
|
||||
this.numberOfRequests++;
|
||||
if (this._continuePolling()) {
|
||||
this._scheduleFetch();
|
||||
}
|
||||
},
|
||||
error: (e) => {
|
||||
_.isFunction(this.error) && this.error(this.model);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Poller.prototype._continuePolling = function () {
|
||||
return !this.stopWhen ||
|
||||
(_.isFunction(this.stopWhen) && !this.stopWhen(this.model));
|
||||
};
|
||||
|
||||
Poller.prototype.stop = function () {
|
||||
this.polling = false;
|
||||
clearTimeout(this.timeout);
|
||||
delete this.timeout;
|
||||
};
|
||||
|
||||
module.exports = Poller;
|
||||
117
lib/assets/javascripts/dashboard/data/config-model.js
Normal file
117
lib/assets/javascripts/dashboard/data/config-model.js
Normal file
@@ -0,0 +1,117 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Model for general frontend configuration.
|
||||
* Ported from old cdb.config, since we can't reuse the older model that's tied to v3 of cartodb.js
|
||||
*
|
||||
* Also, rather than putting it as a global object, it's intended to be instantiated at the entry point and passed as
|
||||
* a collaborator object the models that needs it, e.g.:
|
||||
* var myModel = new MyModel({ id: 123, … }, {
|
||||
* configModel: configModel
|
||||
* })
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
VERSION: 2,
|
||||
|
||||
initialize: function () {
|
||||
this.modules = new Backbone.Collection();
|
||||
this.modules.on('add', function (model) {
|
||||
this.trigger('moduleLoaded');
|
||||
this.trigger('moduleLoaded:' + model.get('name'));
|
||||
}, this);
|
||||
},
|
||||
|
||||
// error track
|
||||
REPORT_ERROR_URL: '/api/v0/error',
|
||||
ERROR_TRACK_ENABLED: false,
|
||||
|
||||
/**
|
||||
* returns the base url to compose the final url
|
||||
* http://user.carto.com/
|
||||
*/
|
||||
getSqlApiBaseUrl: function () {
|
||||
var url;
|
||||
if (this.get('sql_api_template')) {
|
||||
url = this.get('sql_api_template').replace('{user}', this.get('user_name'));
|
||||
} else {
|
||||
url = this.get('sql_api_protocol') + '://' +
|
||||
this.get('user_name') + '.' +
|
||||
this.get('sql_api_domain') + ':' +
|
||||
this.get('sql_api_port');
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
/**
|
||||
* returns the full sql api url, including the api endpoint
|
||||
* allos to specify the version
|
||||
* http://user.carto.com/api/v1/sql
|
||||
*/
|
||||
getSqlApiUrl: function (version) {
|
||||
version = version || 'v2';
|
||||
return this.getSqlApiBaseUrl() + '/api/' + version + '/sql';
|
||||
},
|
||||
|
||||
/**
|
||||
* returns the maps api host, removing user template
|
||||
* and the protocol.
|
||||
* carto.com:3333
|
||||
*/
|
||||
getMapsApiHost: function () {
|
||||
var url;
|
||||
var mapsApiTemplate = this.get('maps_api_template');
|
||||
if (mapsApiTemplate) {
|
||||
url = mapsApiTemplate.replace(/https?:\/\/{user}\./, '');
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
setUrlVersion: function (modelName, method, v) {
|
||||
this.set(modelName + '_' + method + '_url_version', v || 'v1');
|
||||
},
|
||||
|
||||
urlVersion: function (modelName, method, defaultVersion) {
|
||||
method = method || '';
|
||||
var version = this.get(modelName + '_' + method + '_url_version');
|
||||
return version || defaultVersion || 'v1';
|
||||
},
|
||||
|
||||
prefixUrl: function () {
|
||||
return this.get('url_prefix') || '';
|
||||
},
|
||||
|
||||
prefixUrlPathname: function () {
|
||||
var prefix = this.prefixUrl();
|
||||
if (prefix !== '') {
|
||||
try {
|
||||
if (prefix && prefix.indexOf('/') === -1) throw new TypeError('invalid URL');
|
||||
var a = document.createElement('a');
|
||||
a.href = prefix;
|
||||
var url = a.pathname;
|
||||
// remove trailing slash
|
||||
return url.replace(/\/$/, '');
|
||||
} catch (e) {
|
||||
// not an url
|
||||
}
|
||||
}
|
||||
return prefix;
|
||||
},
|
||||
|
||||
getMapsResourceName: function (username) {
|
||||
var url;
|
||||
var mapsApiTemplate = this.get('maps_api_template');
|
||||
if (mapsApiTemplate) {
|
||||
url = mapsApiTemplate.replace(/(http|https)?:\/\//, '').replace(/{user}/g, username);
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
dataLibraryEnabled: function () {
|
||||
return this.get('data_library_enabled');
|
||||
},
|
||||
|
||||
isHosted: function () {
|
||||
return this.get('cartodb_com_hosted');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
var DashboardVisUrlModel = require('dashboard/data/dashboard-vis-url-model');
|
||||
|
||||
/**
|
||||
* URL representing dashboard datasets
|
||||
*/
|
||||
var DashboardDatasetsUrlModel = DashboardVisUrlModel.extend({
|
||||
|
||||
dataLibrary: function () {
|
||||
return this.urlToPath('library');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = DashboardDatasetsUrlModel;
|
||||
28
lib/assets/javascripts/dashboard/data/dashboard-url-model.js
Normal file
28
lib/assets/javascripts/dashboard/data/dashboard-url-model.js
Normal file
@@ -0,0 +1,28 @@
|
||||
var UrlModel = require('dashboard/data/url-model');
|
||||
var DashboardVisUrlModel = require('dashboard/data/dashboard-vis-url-model');
|
||||
var DashboardDatasetsUrlModel = require('dashboard/data/dashboard-datasets-url-model');
|
||||
/**
|
||||
* URLs associated with the dashboard.
|
||||
*/
|
||||
var DashboardUrlModel = UrlModel.extend({
|
||||
|
||||
datasets: function () {
|
||||
return new DashboardDatasetsUrlModel({
|
||||
base_url: this.urlToPath('datasets')
|
||||
});
|
||||
},
|
||||
|
||||
maps: function () {
|
||||
return new DashboardVisUrlModel({
|
||||
base_url: this.urlToPath('maps')
|
||||
});
|
||||
},
|
||||
|
||||
deepInsights: function () {
|
||||
return new DashboardVisUrlModel({
|
||||
base_url: this.urlToPath('deep-insights')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = DashboardUrlModel;
|
||||
@@ -0,0 +1,20 @@
|
||||
var UrlModel = require('dashboard/data/url-model');
|
||||
|
||||
/**
|
||||
* URLs associated with the dashboard visualizations.
|
||||
*/
|
||||
var DashboardVisUrl = UrlModel.extend({
|
||||
lockedItems: function () {
|
||||
return this.urlToPath('locked');
|
||||
},
|
||||
|
||||
sharedItems: function () {
|
||||
return this.urlToPath('shared');
|
||||
},
|
||||
|
||||
likedItems: function () {
|
||||
return this.urlToPath('liked');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = DashboardVisUrl;
|
||||
17
lib/assets/javascripts/dashboard/data/dataset-url-model.js
Normal file
17
lib/assets/javascripts/dashboard/data/dataset-url-model.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const UrlModel = require('dashboard/data/url-model');
|
||||
|
||||
/**
|
||||
* URL for a dataset (standard vis).
|
||||
*/
|
||||
const DatasetUrlModel = UrlModel.extend({
|
||||
|
||||
edit: function () {
|
||||
return this.urlToPath();
|
||||
},
|
||||
|
||||
public: function () {
|
||||
return this.urlToPath('public');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = DatasetUrlModel;
|
||||
23
lib/assets/javascripts/dashboard/data/datasets-collection.js
Normal file
23
lib/assets/javascripts/dashboard/data/datasets-collection.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const VisualizationsCollection = require('dashboard/data/visualizations-collection');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
module.exports = VisualizationsCollection.extend({
|
||||
_ITEMS_PER_PAGE: 12,
|
||||
|
||||
initialize: function (models, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
VisualizationsCollection.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
url: function () {
|
||||
const host = `${this._configModel.get('common_data_user')}.${this._configModel.get('account_host')}`;
|
||||
const options = this._createUrlOptions();
|
||||
|
||||
return `//${host}/api/v1/viz/?${options}`;
|
||||
}
|
||||
});
|
||||
25
lib/assets/javascripts/dashboard/data/flash-message-model.js
Normal file
25
lib/assets/javascripts/dashboard/data/flash-message-model.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
msg: '',
|
||||
type: 'error',
|
||||
display: false
|
||||
},
|
||||
|
||||
shouldDisplay: function () {
|
||||
return this.get('display') && !!this.get('msg') && !!this.get('type');
|
||||
},
|
||||
|
||||
show: function (message, type) {
|
||||
return this.set({
|
||||
display: true,
|
||||
msg: message,
|
||||
type: type
|
||||
});
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.set('display', false);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
const Backbone = require('backbone');
|
||||
const _ = require('underscore');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const UserModel = require('dashboard/data/user-model');
|
||||
const GroupModel = require('dashboard/data/group-model');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* A collection of Grantable objects.
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (attrs, { collection }) {
|
||||
// This used to be in its own file, but we took the same approach as builder
|
||||
|
||||
// This used to have:
|
||||
// new cdb.admin[className](this.get('model'));
|
||||
// We took grantable_presenter types as the truth.
|
||||
|
||||
const { _configModel: configModel, organization } = collection;
|
||||
|
||||
let model;
|
||||
if (attrs.type === 'user') {
|
||||
model = new UserModel(attrs, { configModel, collection });
|
||||
} else {
|
||||
model = new GroupModel(attrs, { configModel, collection });
|
||||
}
|
||||
model.organization = organization;
|
||||
model.entity = model; // legacy (see grantable.js), left in case someone uses it
|
||||
return model;
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('organizationGrantables', method);
|
||||
return '/api/' + version + '/organization/' + this.organization.id + '/grantables';
|
||||
},
|
||||
|
||||
initialize: function (users, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
if (!opts.organization) throw new Error('organization is required');
|
||||
this.organization = opts.organization;
|
||||
this.currentUserId = opts.currentUserId;
|
||||
this.sync = require('dashboard/data/backbone/sync-abort'); // adds abort behaviour
|
||||
},
|
||||
|
||||
parse: function (response) {
|
||||
this.total_entries = response.total_entries;
|
||||
|
||||
return _.reduce(response.grantables, function (memo, m) {
|
||||
if (m.id === this.currentUserId) {
|
||||
this.total_entries--;
|
||||
} else {
|
||||
memo.push(m);
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, [], this);
|
||||
},
|
||||
|
||||
// @return {Number, undefined} may be undefined until a first fetch is done
|
||||
totalCount: function () {
|
||||
return this.total_entries;
|
||||
}
|
||||
|
||||
});
|
||||
35
lib/assets/javascripts/dashboard/data/group-model.js
Normal file
35
lib/assets/javascripts/dashboard/data/group-model.js
Normal file
@@ -0,0 +1,35 @@
|
||||
var Backbone = require('backbone');
|
||||
var GroupUsersCollection = require('dashboard/data/group-users-collection');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Model representing a group.
|
||||
* Expected to be used in the context of a groups collection (e.g. cdb.admin.OrganizationGroups),
|
||||
* which defines its API endpoint path.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
display_name: '' // UI name, as given by
|
||||
// name: '', // internal alphanumeric representation, converted from display_name internally
|
||||
// organization_id: '',
|
||||
},
|
||||
|
||||
initialize: function (attrs, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this.parse(attrs || {}); // handle given attrs in the same way as for .fetch()
|
||||
},
|
||||
|
||||
parse: function (attrs, options) {
|
||||
this.users = new GroupUsersCollection(attrs.users, {
|
||||
group: this,
|
||||
configModel: this._configModel
|
||||
});
|
||||
return attrs;
|
||||
},
|
||||
|
||||
getModelType: () => 'group'
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const Backbone = require('backbone');
|
||||
const User = require('dashboard/data/user-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* A collection representing a set of users in a group.
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: User,
|
||||
|
||||
initialize: function (models, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
if (!opts.group) throw new Error('group is required');
|
||||
this.group = opts.group;
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return this.group.url.apply(this.group, arguments) + '/users';
|
||||
},
|
||||
|
||||
parse: function (response) {
|
||||
this.total_entries = response.total_entries;
|
||||
this.total_user_entries = response.total_user_entries;
|
||||
|
||||
return response.users;
|
||||
},
|
||||
|
||||
/**
|
||||
* Batch add users
|
||||
* @param {Array} userIds
|
||||
* @return {Object} a deferred jqXHR object
|
||||
*/
|
||||
addInBatch: function (userIds, password) {
|
||||
return this._batchAsyncProcessUsers('POST', userIds, password);
|
||||
},
|
||||
|
||||
removeInBatch: function (userIds, password) {
|
||||
var self = this;
|
||||
return this._batchAsyncProcessUsers('DELETE', userIds, password)
|
||||
.done(function () {
|
||||
_.each(userIds, self.remove.bind(self));
|
||||
});
|
||||
},
|
||||
|
||||
_batchAsyncProcessUsers: function (method, ids, password) {
|
||||
var self = this;
|
||||
|
||||
// postpone relving promise since the fetch is requries for collection to have accurate state
|
||||
var deferred = $.Deferred();
|
||||
$.ajax({
|
||||
type: method,
|
||||
url: this._configModel.get('base_url') + this.url(),
|
||||
data: {
|
||||
users: ids,
|
||||
password_confirmation: password
|
||||
},
|
||||
success: function () {
|
||||
var args = arguments;
|
||||
|
||||
// because add/remove don't return any data, so need to fetch to get accurate state
|
||||
self.fetch({
|
||||
success: function () {
|
||||
deferred.resolve.apply(deferred, args);
|
||||
},
|
||||
error: function () {
|
||||
// could not update state, but resolve anyway since batch operation worked
|
||||
// might have inconsistent state though
|
||||
deferred.resolve.apply(deferred, args);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function () {
|
||||
deferred.reject.apply(deferred, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred;
|
||||
},
|
||||
|
||||
// @return {Number, undefined} may be undefined until a first fetch is done
|
||||
totalCount: function () {
|
||||
return this.total_user_entries;
|
||||
}
|
||||
|
||||
});
|
||||
51
lib/assets/javascripts/dashboard/data/import-model.js
Normal file
51
lib/assets/javascripts/dashboard/data/import-model.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
idAttribute: 'item_queue_id',
|
||||
|
||||
urlRoot: '/api/v1/imports',
|
||||
|
||||
initialize: function () {
|
||||
this.bind('change', this._checkFinish, this);
|
||||
},
|
||||
|
||||
setUrlRoot: function (urlRoot) {
|
||||
this.urlRoot = urlRoot;
|
||||
},
|
||||
|
||||
/**
|
||||
* checks for poll to finish
|
||||
*/
|
||||
pollCheck: function (i) {
|
||||
var self = this;
|
||||
this.pollTimer = setInterval(function () {
|
||||
// cdb.log.debug("checking job for finish: " + tries);
|
||||
self.fetch({
|
||||
error: function (e) {
|
||||
self.trigger('change');
|
||||
}
|
||||
});
|
||||
}, i || 1500);
|
||||
},
|
||||
|
||||
destroyCheck: function () {
|
||||
clearInterval(this.pollTimer);
|
||||
},
|
||||
|
||||
_checkFinish: function () {
|
||||
// cdb.log.info("state: " + this.get('state'), "success: " + this.get("success"));
|
||||
|
||||
if (this.get('success') === true) {
|
||||
// cdb.log.debug("job finished");
|
||||
clearInterval(this.pollTimer);
|
||||
this.trigger('importComplete', this);
|
||||
} else if (this.get('success') === false) {
|
||||
// cdb.log.debug("job failure");
|
||||
clearInterval(this.pollTimer);
|
||||
this.trigger('importError', this);
|
||||
} else {
|
||||
this.trigger('importChange', this);
|
||||
}
|
||||
}
|
||||
});
|
||||
113
lib/assets/javascripts/dashboard/data/imports-collection.js
Normal file
113
lib/assets/javascripts/dashboard/data/imports-collection.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const ImportsModel = require('dashboard/data/imports-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const pollTimer = 30000;
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'userModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Imports collection
|
||||
*
|
||||
* If it is fetched, it will add the import
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (attrs, options) {
|
||||
return new ImportsModel(attrs, {
|
||||
userModel: options.collection._userModel,
|
||||
configModel: options.collection._configModel
|
||||
});
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
const version = this._configModel.urlVersion('import', method);
|
||||
return '/api/' + version + '/imports';
|
||||
},
|
||||
|
||||
initialize: function (models, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
parse: function (r) {
|
||||
if (r.imports.length === 0) {
|
||||
this.destroyCheck();
|
||||
} else {
|
||||
_.each(r.imports, id => {
|
||||
// Check if that import exists...
|
||||
var imports = this.filter(mdl => mdl._importModel.get('item_queue_id') === id);
|
||||
|
||||
if (imports.length === 0) {
|
||||
this.add(new ImportsModel({ id: id }, {
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.models;
|
||||
},
|
||||
|
||||
canImport: function () {
|
||||
const importQuota = this._userModel.getMaxConcurrentImports();
|
||||
const total = this.size();
|
||||
let finished = 0;
|
||||
|
||||
this.each(function (m) {
|
||||
if (m.hasFailed() || m.hasCompleted()) {
|
||||
++finished;
|
||||
}
|
||||
});
|
||||
|
||||
return (total - finished) < importQuota;
|
||||
},
|
||||
|
||||
pollCheck: function (i) {
|
||||
if (this.pollTimer) return;
|
||||
|
||||
this.pollTimer = setInterval(() => {
|
||||
this.fetch();
|
||||
}, pollTimer || 2000);
|
||||
|
||||
// Start doing a fetch
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
destroyCheck: function () {
|
||||
clearInterval(this.pollTimer);
|
||||
delete this.pollTimer;
|
||||
},
|
||||
|
||||
completedItems: function () {
|
||||
return this.filter(function (item) {
|
||||
return item.hasCompleted();
|
||||
});
|
||||
},
|
||||
|
||||
getCompletedItemsCount: function () {
|
||||
return this.completedItems().length;
|
||||
},
|
||||
|
||||
failedItems: function () {
|
||||
return this.filter(function (item) {
|
||||
return item.hasFailed();
|
||||
});
|
||||
},
|
||||
|
||||
getFailedItemsCount: function () {
|
||||
return this.failedItems().length;
|
||||
},
|
||||
|
||||
allImportsCompletedOrFailed: function () {
|
||||
return this.all(function (item) {
|
||||
return item.hasCompleted() ||
|
||||
item.hasFailed();
|
||||
});
|
||||
}
|
||||
});
|
||||
276
lib/assets/javascripts/dashboard/data/imports-model.js
Normal file
276
lib/assets/javascripts/dashboard/data/imports-model.js
Normal file
@@ -0,0 +1,276 @@
|
||||
// TODO: Hacer un extend del archivo de builder
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var ImportModel = require('builder/data/background-importer/import-model');
|
||||
var UploadModel = require('dashboard/data/upload-model');
|
||||
var VisualizationModel = require('dashboard/data/visualization-model');
|
||||
var PermissionModel = require('dashboard/data/permission-model');
|
||||
|
||||
/**
|
||||
* Upload/import model
|
||||
*
|
||||
* It takes the control of the upload and import,
|
||||
* listening the change of any of these steps.
|
||||
*
|
||||
* Steps:
|
||||
* - upload
|
||||
* - import
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
step: 'upload',
|
||||
state: ''
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._userModel = opts.userModel;
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
this._uploadModel = new UploadModel(opts.upload, {
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
if (_.isEmpty(opts)) {
|
||||
opts = {};
|
||||
}
|
||||
|
||||
this._importModel = new ImportModel(opts.import, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
this._initBinds();
|
||||
this._checkStatus();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('change:import', this._onImportChange, this);
|
||||
this.bind('change:upload', this._onUploadChange, this);
|
||||
this.bind('change:id', this._onIdChange, this);
|
||||
|
||||
this._importModel.bind('change', function () {
|
||||
this.trigger('change:import', this);
|
||||
this.trigger('change', this);
|
||||
}, this);
|
||||
|
||||
this._uploadModel.bind('change', function () {
|
||||
this.trigger('change:upload', this);
|
||||
this.trigger('change', this);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_destroyBinds: function () {
|
||||
this._uploadModel.unbind(null, null, this);
|
||||
this._importModel.unbind(null, null, this);
|
||||
},
|
||||
|
||||
_onIdChange: function () {
|
||||
var item_queue_id = this.get('id');
|
||||
if (item_queue_id) this._importModel.set('item_queue_id', item_queue_id);
|
||||
this.set('step', 'import');
|
||||
},
|
||||
|
||||
_onUploadChange: function (m, i) {
|
||||
if (this.get('step') === 'upload') {
|
||||
var item_queue_id = this._uploadModel.get('item_queue_id');
|
||||
var state = this._uploadModel.get('state');
|
||||
|
||||
if (item_queue_id) this.set('id', item_queue_id);
|
||||
if (state) this.set('state', state);
|
||||
}
|
||||
},
|
||||
|
||||
_onImportChange: function () {
|
||||
if (this.get('step') === 'import') {
|
||||
var state = this._importModel.get('state');
|
||||
if (state) this.set('state', state);
|
||||
}
|
||||
},
|
||||
|
||||
_checkStatus: function () {
|
||||
if (!this.get('id') && !this._uploadModel.isValid()) {
|
||||
this.trigger('change:upload');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._uploadModel.get('type') === 'file') {
|
||||
this._uploadModel.upload();
|
||||
} else if (this.get('id')) {
|
||||
this.set('step', 'import');
|
||||
this._importModel.set('item_queue_id', this.get('id'));
|
||||
} else if (!this._importModel.get('item_queue_id') && this._uploadModel.get('type') !== '') {
|
||||
this.set('step', 'import');
|
||||
this._importModel.createImport(this._uploadModel.toJSON());
|
||||
}
|
||||
},
|
||||
|
||||
getImportModel: function () {
|
||||
return this._importModel;
|
||||
},
|
||||
|
||||
pause: function () {
|
||||
this.stopUpload();
|
||||
this.stopImport();
|
||||
},
|
||||
|
||||
hasFailed: function () {
|
||||
var state = this.get('state');
|
||||
var step = this.get('step');
|
||||
|
||||
return (step === 'import' && state === 'failure') || (step === 'upload' && state === 'error');
|
||||
},
|
||||
|
||||
hasCompleted: function () {
|
||||
return this.get('step') === 'import' && this._importModel && this._importModel.get('state') === 'complete';
|
||||
},
|
||||
|
||||
getWarnings: function () {
|
||||
return this.get('step') === 'import' ? this._importModel.get('warnings') : '';
|
||||
},
|
||||
|
||||
getError: function () {
|
||||
if (this.hasFailed()) {
|
||||
var step = this.get('step');
|
||||
return _.extend(
|
||||
{
|
||||
errorCode: this[step === 'upload' ? '_uploadModel' : '_importModel'].get('error_code'),
|
||||
itemQueueId: step === 'import' ? this._importModel.get('id') : '',
|
||||
originalUrl: step === 'import' ? this._importModel.get('original_url') : '',
|
||||
dataType: step === 'import' ? this._importModel.get('data_type') : '',
|
||||
httpResponseCode: step === 'import' ? this._importModel.get('http_response_code') : '',
|
||||
httpResponseCodeMessage: step === 'import' ? this._importModel.get('http_response_code_message') : ''
|
||||
},
|
||||
this[step === 'upload' ? '_uploadModel' : '_importModel'].get('get_error_text'));
|
||||
}
|
||||
|
||||
return {
|
||||
title: '',
|
||||
what_about: '',
|
||||
error_code: ''
|
||||
};
|
||||
},
|
||||
|
||||
importedVis: function () {
|
||||
if (this.get('import').derived_visualization_id) {
|
||||
return this._getMapVis();
|
||||
} else {
|
||||
return this._getDatasetVis();
|
||||
}
|
||||
},
|
||||
|
||||
getNumberOfTablesCreated: function () {
|
||||
return this._importModel.get('tables_created_count');
|
||||
},
|
||||
|
||||
_getServiceName: function () {
|
||||
return this._importModel.get('service_name');
|
||||
},
|
||||
|
||||
isTwitterImport: function () {
|
||||
return this._getServiceName() === 'twitter_search';
|
||||
},
|
||||
|
||||
isCartoImport: function () {
|
||||
return this._getDisplayName() && this._getDisplayName().match(/\.carto$/i);
|
||||
},
|
||||
|
||||
_getDisplayName: function () {
|
||||
return this._importModel.get('display_name');
|
||||
},
|
||||
|
||||
_getMapVis: function () {
|
||||
var derivedVisId = this._importModel.get('derived_visualization_id');
|
||||
|
||||
if (!derivedVisId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._createVis({
|
||||
type: 'derived',
|
||||
id: derivedVisId
|
||||
});
|
||||
},
|
||||
|
||||
_getDatasetVis: function () {
|
||||
var tableName = this._importModel.get('table_name');
|
||||
|
||||
if (!tableName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._createVis({
|
||||
type: 'table',
|
||||
table: {
|
||||
name: tableName
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_createVis: function (attrs) {
|
||||
var vis = new VisualizationModel(attrs, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
vis.permission = new PermissionModel({
|
||||
owner: this._userModel.attributes
|
||||
}, {
|
||||
configModel: this._configModel,
|
||||
userModel: this._userModel
|
||||
});
|
||||
|
||||
return vis;
|
||||
},
|
||||
|
||||
setError: function (opts) {
|
||||
var stepModel = this[this.get('step') === 'upload' ? '_uploadModel' : '_importModel'];
|
||||
|
||||
this.stopUpload();
|
||||
this.stopImport();
|
||||
|
||||
stepModel.set(opts);
|
||||
|
||||
this.set('state', 'error');
|
||||
},
|
||||
|
||||
stopUpload: function () {
|
||||
this._uploadModel.stopUpload();
|
||||
},
|
||||
|
||||
stopImport: function () {
|
||||
this._importModel.destroyCheck();
|
||||
},
|
||||
|
||||
get: function (attr) {
|
||||
if (attr === 'upload') {
|
||||
return this._uploadModel.toJSON();
|
||||
}
|
||||
|
||||
if (attr === 'import') {
|
||||
return this._importModel.toJSON();
|
||||
}
|
||||
|
||||
return Backbone.Model.prototype.get.call(this, attr);
|
||||
},
|
||||
|
||||
getRedirectUrl: function (user) {
|
||||
var vis = this.importedVis();
|
||||
if (vis) {
|
||||
return encodeURI(vis.viewUrl(user).edit());
|
||||
}
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return {
|
||||
step: this.get('step'),
|
||||
id: this.get('id'),
|
||||
state: this.get('state'),
|
||||
upload: this._uploadModel.toJSON(),
|
||||
import: this._importModel.toJSON()
|
||||
};
|
||||
}
|
||||
});
|
||||
70
lib/assets/javascripts/dashboard/data/layers-collection.js
Normal file
70
lib/assets/javascripts/dashboard/data/layers-collection.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const Backbone = require('backbone');
|
||||
const MapLayer = require('dashboard/data/map-layer-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const TILED_LAYER_TYPE = 'Tiled';
|
||||
const CARTODB_LAYER_TYPE = 'CartoDB';
|
||||
const TORQUE_LAYER_TYPE = 'torque';
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
const LayersCollection = Backbone.Collection.extend({
|
||||
model: MapLayer,
|
||||
|
||||
initialize: function (models, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.comparator = function (m) {
|
||||
return parseInt(m.get('order'), 10);
|
||||
};
|
||||
this.bind('add', this._assignIndexes);
|
||||
this.bind('remove', this._assignIndexes);
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('layer', method);
|
||||
return `/api/${version}/maps/${this.map.id}/layers`;
|
||||
},
|
||||
|
||||
parse: function (data) {
|
||||
return data.layers;
|
||||
},
|
||||
|
||||
/**
|
||||
* each time a layer is added or removed
|
||||
* the index should be recalculated
|
||||
*/
|
||||
_assignIndexes: function (model, col, options) {
|
||||
if (this.size() > 0) {
|
||||
// Assign an order of 0 to the first layer
|
||||
this.at(0).set({ order: 0 });
|
||||
|
||||
if (this.size() > 1) {
|
||||
var layersByType = {};
|
||||
for (var i = 1; i < this.size(); ++i) {
|
||||
var layer = this.at(i);
|
||||
var layerType = layer.get('type');
|
||||
layersByType[layerType] = layersByType[layerType] || [];
|
||||
layersByType[layerType].push(layer);
|
||||
}
|
||||
|
||||
var lastOrder = 0;
|
||||
var sortedTypes = [CARTODB_LAYER_TYPE, TORQUE_LAYER_TYPE, TILED_LAYER_TYPE];
|
||||
for (var index = 0; index < sortedTypes.length; ++index) {
|
||||
var type = sortedTypes[index];
|
||||
var layers = layersByType[type] || [];
|
||||
for (var j = 0; j < layers.length; ++j) {
|
||||
var layerModel = layers[j];
|
||||
layerModel.set({
|
||||
order: ++lastOrder
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = LayersCollection;
|
||||
68
lib/assets/javascripts/dashboard/data/like-model.js
Normal file
68
lib/assets/javascripts/dashboard/data/like-model.js
Normal file
@@ -0,0 +1,68 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'config'
|
||||
];
|
||||
|
||||
var LikeModel = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
likeable: true
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var version = this._config.urlVersion('like', method);
|
||||
return `${this._config.get('base_url')}/api/${version}/viz/${this.get('vis_id')}/like`;
|
||||
},
|
||||
|
||||
initialize: function (attrs, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
_.bindAll(this, '_onSaveError');
|
||||
|
||||
this.on('destroy', function () {
|
||||
this.set({
|
||||
liked: false,
|
||||
likes: this.get('likes') - 1
|
||||
});
|
||||
}, this);
|
||||
},
|
||||
|
||||
_onSaveError: function (model, response) {
|
||||
this.trigger('error', {
|
||||
status: response.status,
|
||||
statusText: response.statusText
|
||||
});
|
||||
},
|
||||
|
||||
toggleLiked: function () {
|
||||
if (this.get('liked')) {
|
||||
this.destroy();
|
||||
} else {
|
||||
this.set({ id: null }, { silent: true });
|
||||
this.save({}, { error: this._onSaveError });
|
||||
}
|
||||
}
|
||||
|
||||
}, {
|
||||
|
||||
newByVisData: function (opts) {
|
||||
var d = _.defaults({
|
||||
id: opts.liked ? opts.vis_id : null
|
||||
}, _.omit(opts, 'url', 'config'));
|
||||
|
||||
var model = new LikeModel(d, {
|
||||
config: opts.config
|
||||
});
|
||||
|
||||
if (opts.url) {
|
||||
model.url = opts.url;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = LikeModel;
|
||||
10
lib/assets/javascripts/dashboard/data/map-layer-model.js
Normal file
10
lib/assets/javascripts/dashboard/data/map-layer-model.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
const MapLayer = Backbone.Model.extend({
|
||||
defaults: {
|
||||
visible: true,
|
||||
type: 'Tiled'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = MapLayer;
|
||||
26
lib/assets/javascripts/dashboard/data/map-model.js
Normal file
26
lib/assets/javascripts/dashboard/data/map-model.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const Backbone = require('backbone');
|
||||
const LayersCollection = require('dashboard/data/layers-collection');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
const MapModel = Backbone.Model.extend({
|
||||
urlRoot: '/api/v1/maps',
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this.bind('change:id', this._fetchLayers, this);
|
||||
|
||||
this.layers = new LayersCollection(null, { configModel: this._configModel });
|
||||
this.layers.map = this;
|
||||
},
|
||||
|
||||
// fetch related layers
|
||||
_fetchLayers: function () {
|
||||
this.layers.fetch();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = MapModel;
|
||||
21
lib/assets/javascripts/dashboard/data/map-url-model.js
Normal file
21
lib/assets/javascripts/dashboard/data/map-url-model.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const UrlModel = require('dashboard/data/url-model');
|
||||
|
||||
/**
|
||||
* URL for a map (derived vis).
|
||||
*/
|
||||
const MapUrlModel = UrlModel.extend({
|
||||
|
||||
edit: function () {
|
||||
return this.urlToPath('map');
|
||||
},
|
||||
|
||||
public: function () {
|
||||
return this.urlToPath('public_map');
|
||||
},
|
||||
|
||||
deepInsights: function () {
|
||||
return this.urlToPath('deep-insights');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = MapUrlModel;
|
||||
@@ -0,0 +1,54 @@
|
||||
const Backbone = require('backbone');
|
||||
const GroupModel = require('dashboard/data/group-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* A collection that holds a set of organization groups
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (attrs, { collection }) {
|
||||
return new GroupModel(attrs, {
|
||||
collection,
|
||||
configModel: collection._configModel
|
||||
});
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('organizationGroups', method);
|
||||
return '/api/' + version + '/organization/' + this.organization.id + '/groups';
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
if (!opts.organization) throw new Error('organization is required');
|
||||
this.organization = opts.organization;
|
||||
},
|
||||
|
||||
parse: function (response) {
|
||||
this.total_entries = response.total_entries;
|
||||
return response.groups;
|
||||
},
|
||||
|
||||
// @return {Object} A instance of cdb.admin.Group. If group wasn't already present a new model with id and collection
|
||||
// set will be returned, i.e. group.fetch() will be required to get the data or handle the err case (e.g. non-existing)
|
||||
newGroupById: function (id) {
|
||||
var group = this.get(id);
|
||||
if (!group) {
|
||||
group = new GroupModel({
|
||||
id: id
|
||||
}, { configModel: this._configModel, collection: this });
|
||||
}
|
||||
return group;
|
||||
},
|
||||
|
||||
// @return {Number, undefined} may be undefined until a first fetch is done
|
||||
totalCount: function () {
|
||||
return this.total_entries;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'organizationId',
|
||||
'configModel'
|
||||
];
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
users_emails: []
|
||||
},
|
||||
|
||||
initialize: function (attrs, options) {
|
||||
this.attributes['welcome_text'] = 'I\'d like to invite you to my ' + options.configModel['attributes'].app_name + ' organization,\nBest regards';
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return `/api/v1/organization/${this._organizationId}/invitations`;
|
||||
}
|
||||
});
|
||||
76
lib/assets/javascripts/dashboard/data/organization-model.js
Normal file
76
lib/assets/javascripts/dashboard/data/organization-model.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const OrganizationUsersCollection = require('dashboard/data/organization-users-collection');
|
||||
const GrantablesCollection = require('dashboard/data/grantables-collection');
|
||||
const OrganizationGroupsCollection = require('dashboard/data/organization-groups-collection');
|
||||
const OrganizationUrl = require('dashboard/data/organization-url-model');
|
||||
const UserModel = require('dashboard/data/user-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* this model contains information about the organization for
|
||||
* the current user and the users who are inside the organizacion.
|
||||
*
|
||||
* Attributes:
|
||||
* - users: collection with user instances within the organization (see cdb.admin.Organization.Users
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
url: '/api/v1/org/',
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
attrs = attrs || {};
|
||||
|
||||
this.owner = new UserModel(this.get('owner'));
|
||||
|
||||
// ESlint errors on the below line ported as they were (== instead of ===)
|
||||
this.display_email = (typeof attrs.admin_email !== 'undefined') && attrs.admin_email != null && (attrs.admin_email == '' ? this.owner.email : attrs.admin_email); // eslint-disable-line
|
||||
|
||||
var collectionOpts = {
|
||||
organization: this,
|
||||
currentUserId: opts && opts.currentUserId,
|
||||
configModel: this._configModel
|
||||
};
|
||||
this.users = new OrganizationUsersCollection(attrs.users, collectionOpts);
|
||||
this.groups = new OrganizationGroupsCollection(attrs.groups, collectionOpts);
|
||||
this.grantables = new GrantablesCollection(undefined, collectionOpts);
|
||||
|
||||
// make sure all the users/groups have a reference to this organization
|
||||
this.users.each(this._setOrganizationOnModel, this);
|
||||
this.groups.each(this._setOrganizationOnModel, this);
|
||||
},
|
||||
|
||||
_setOrganizationOnModel: function (m) {
|
||||
m.organization = this;
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
throw new Error('organization should not be fetch, should be static');
|
||||
},
|
||||
|
||||
containsUser: function (user) {
|
||||
return !!this.users.find(function (u) {
|
||||
return u.id === user.id;
|
||||
});
|
||||
},
|
||||
|
||||
isOrgAdmin: function (user) {
|
||||
return this.owner.id === user.id || !!_.find(this.get('admins'), function (u) {
|
||||
return u.id === user.id;
|
||||
});
|
||||
},
|
||||
|
||||
viewUrl: function () {
|
||||
return new OrganizationUrl({
|
||||
base_url: this.get('base_url')
|
||||
});
|
||||
},
|
||||
|
||||
getModelType: () => 'org'
|
||||
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
var UrlModel = require('dashboard/data/url-model');
|
||||
|
||||
/**
|
||||
* URL for a map (derived vis).
|
||||
*/
|
||||
var OrganizationUrlModel = UrlModel.extend({
|
||||
edit: function (user) {
|
||||
if (!user) {
|
||||
throw new Error('User is needed to create the url');
|
||||
}
|
||||
return this.urlToPath(user.get('username') + '/edit');
|
||||
},
|
||||
|
||||
create: function () {
|
||||
return this.urlToPath('new');
|
||||
},
|
||||
|
||||
groups: function () {
|
||||
return this.urlToPath('groups');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = OrganizationUrlModel;
|
||||
@@ -0,0 +1,64 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const UserModel = require('dashboard/data/user-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'organization',
|
||||
'configModel'
|
||||
];
|
||||
|
||||
// helper to manage organization users
|
||||
module.exports = Backbone.Collection.extend({
|
||||
model: UserModel,
|
||||
|
||||
_DEFAULT_EXCLUDE_CURRENT_USER: true,
|
||||
|
||||
url: function () {
|
||||
return '/api/v1/organization/' + this.organization.id + '/users';
|
||||
},
|
||||
|
||||
initialize: function (models, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.organization = this._organization;
|
||||
this.currentUserId = options.currentUserId;
|
||||
this._excludeCurrentUser = this._DEFAULT_EXCLUDE_CURRENT_USER;
|
||||
},
|
||||
|
||||
comparator: function (model) {
|
||||
return model.get('username');
|
||||
},
|
||||
|
||||
excludeCurrentUser: function (exclude) {
|
||||
exclude = !!exclude;
|
||||
this._excludeCurrentUser = exclude;
|
||||
if (exclude && !this.currentUserId) {
|
||||
console.error('set excludeCurrentUser to true, but there is no current user id set to exclude!');
|
||||
}
|
||||
},
|
||||
|
||||
restoreExcludeCurrentUser: function () {
|
||||
this.excludeCurrentUser(this._DEFAULT_EXCLUDE_CURRENT_USER);
|
||||
},
|
||||
|
||||
parse: function (r) {
|
||||
this.total_entries = r.total_entries;
|
||||
this.total_user_entries = r.total_user_entries;
|
||||
|
||||
return _.reduce(r.users, function (memo, user) {
|
||||
if (this._excludeCurrentUser && user.id === this.currentUserId) {
|
||||
this.total_user_entries--;
|
||||
this.total_entries--;
|
||||
} else {
|
||||
memo.push(user);
|
||||
}
|
||||
return memo;
|
||||
}, [], this);
|
||||
},
|
||||
|
||||
// @return {Number, undefined} may be undefined until a first fetch is done
|
||||
totalCount: function () {
|
||||
return this.total_user_entries;
|
||||
}
|
||||
});
|
||||
28
lib/assets/javascripts/dashboard/data/paged-search-model.js
Normal file
28
lib/assets/javascripts/dashboard/data/paged-search-model.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Model representing the query string params for a "paged search" of a collection (matching the server-side APIs).
|
||||
*
|
||||
* @example usage
|
||||
* const PagedSearch = require('dashboard/data/paged-search-model');
|
||||
* pagedSearch = new PagedSearch({ … })
|
||||
* pagedSearch.fetch(collection) // => jqXHR, GET /collection/123?page=1&per_page20
|
||||
* pagedSearch.set({ page: 2, per_page: 10, q: 'test' });
|
||||
* pagedSearch.fetch(collection) // => GET /collection/123?page=2&per_page10&q=test
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
per_page: 20,
|
||||
page: 1
|
||||
// order: 'name'
|
||||
// q: '',
|
||||
},
|
||||
|
||||
fetch: function (collection) {
|
||||
collection.trigger('fetching');
|
||||
|
||||
return collection.fetch({
|
||||
data: this.attributes
|
||||
});
|
||||
}
|
||||
});
|
||||
304
lib/assets/javascripts/dashboard/data/permission-model.js
Normal file
304
lib/assets/javascripts/dashboard/data/permission-model.js
Normal file
@@ -0,0 +1,304 @@
|
||||
const _ = require('underscore');
|
||||
const result = require('builder/helpers/utils').result;
|
||||
const Backbone = require('backbone');
|
||||
const UserModel = require('dashboard/data/user-model');
|
||||
const OrganizationModel = require('dashboard/data/organization-model');
|
||||
const GroupModel = require('dashboard/data/group-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
// Nobody else uses this, and it would incur on a circular dependency if moved to a file
|
||||
const ACLItemModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
access: 'r'
|
||||
},
|
||||
|
||||
isOwn: function (model) {
|
||||
return model.id === this.get('entity').id;
|
||||
},
|
||||
|
||||
validate: function (attrs, options) {
|
||||
var p = PermissionModel;
|
||||
if (attrs.access !== p.READ_ONLY && attrs.access !== p.READ_WRITE) {
|
||||
return "access can't take 'r' or 'rw' values";
|
||||
}
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
var entity = _.pick(this.get('entity').toJSON(), 'id', 'username', 'avatar_url', 'name');
|
||||
// translate name to username
|
||||
if (!entity.username) {
|
||||
entity.username = entity.name;
|
||||
delete entity.name;
|
||||
}
|
||||
return {
|
||||
type: this.get('type') || 'user',
|
||||
entity: entity,
|
||||
access: this.get('access')
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* manages a cartodb permission object, it contains:
|
||||
* - owner: an cdb.admin.User instance
|
||||
* - acl: a collection which includes the user and their permission.
|
||||
*
|
||||
* see https://github.com/Vizzuality/cartodb-management/wiki/multiuser-REST-API#permissions-object
|
||||
*
|
||||
* this object is not created to work alone, it should be a member of an object like visualization
|
||||
* table
|
||||
*/
|
||||
const PermissionModel = Backbone.Model.extend({
|
||||
urlRoot: '/api/v1/perm',
|
||||
|
||||
initialize: function (attrs, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this.acl = new Backbone.Collection();
|
||||
this.owner = null;
|
||||
this._generateOwner();
|
||||
this._generateAcl();
|
||||
this.bind('change:owner', this._generateOwner, this);
|
||||
this.bind('change:acl', this._generateAcl, this);
|
||||
},
|
||||
|
||||
_generateOwner: function () {
|
||||
if (!this.owner) {
|
||||
this.owner = new UserModel(undefined, { configModel: this._configModel });
|
||||
}
|
||||
this.owner.set(this.get('owner'));
|
||||
},
|
||||
|
||||
_generateAcl: function () {
|
||||
this.acl.reset([], { silent: true });
|
||||
_.each(this.get('acl'), function (aclItem) {
|
||||
var model;
|
||||
switch (aclItem.type) {
|
||||
case 'user':
|
||||
model = new UserModel(aclItem.entity, { configModel: this._configModel });
|
||||
break;
|
||||
case 'org':
|
||||
model = new OrganizationModel(aclItem.entity, { configModel: this._configModel });
|
||||
break;
|
||||
case 'group':
|
||||
model = new GroupModel(aclItem.entity, { configModel: this._configModel });
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown ACL item type: ' + aclItem.type);
|
||||
}
|
||||
this._grantAccess(model, aclItem.access);
|
||||
}, this);
|
||||
},
|
||||
|
||||
cleanPermissions: function () {
|
||||
this.acl.reset();
|
||||
},
|
||||
|
||||
hasAccess: function (model) {
|
||||
// Having at least read access is the same as having any access
|
||||
return this.hasReadAccess(model);
|
||||
},
|
||||
|
||||
hasReadAccess: function (model) {
|
||||
// If there is a representable ACL item it must be one of at least READ_ONLY access
|
||||
return !!this.findRepresentableAclItem(model);
|
||||
},
|
||||
|
||||
hasWriteAccess: function (model) {
|
||||
var access = result(this.findRepresentableAclItem(model), 'get', 'access');
|
||||
return access === PermissionModel.READ_WRITE;
|
||||
},
|
||||
|
||||
canChangeReadAccess: function (model) {
|
||||
return this._canChangeAccess(model);
|
||||
},
|
||||
|
||||
canChangeWriteAccess: function (model) {
|
||||
return (!model.isBuilder || model.isBuilder()) && this._canChangeAccess(model, function (representableAclItem) {
|
||||
return result(representableAclItem, 'get', 'access') !== PermissionModel.READ_WRITE;
|
||||
});
|
||||
},
|
||||
|
||||
_canChangeAccess: function (model) {
|
||||
var representableAclItem = this.findRepresentableAclItem(model);
|
||||
return this.isOwner(model) || !representableAclItem ||
|
||||
representableAclItem === this._ownAclItem(model) || result(arguments, 1, representableAclItem) || false;
|
||||
},
|
||||
|
||||
grantWriteAccess: function (model) {
|
||||
this._grantAccess(model, this.constructor.READ_WRITE);
|
||||
},
|
||||
|
||||
grantReadAccess: function (model) {
|
||||
this._grantAccess(model, this.constructor.READ_ONLY);
|
||||
},
|
||||
|
||||
revokeWriteAccess: function (model) {
|
||||
// Effectively "downgrades" to READ_ONLY
|
||||
this.grantReadAccess(model);
|
||||
},
|
||||
|
||||
/**
|
||||
* Revokes access to a set of items
|
||||
* @param {Object} model A single model or an array of models
|
||||
*/
|
||||
revokeAccess: function (model) {
|
||||
var aclItem = this._ownAclItem(model);
|
||||
if (aclItem) {
|
||||
this.acl.remove(aclItem);
|
||||
}
|
||||
},
|
||||
|
||||
getOwner: function () {
|
||||
return this.owner;
|
||||
},
|
||||
|
||||
isOwner: function (model) {
|
||||
return _.result(this.owner, 'id') === _.result(model, 'id');
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return {
|
||||
entity: this.get('entity'),
|
||||
acl: this.acl.toJSON()
|
||||
};
|
||||
},
|
||||
|
||||
getUsersWithAnyPermission: function () {
|
||||
return this.acl.chain()
|
||||
.filter(this._hasTypeUser)
|
||||
.map(this._getEntity)
|
||||
.value();
|
||||
},
|
||||
|
||||
isSharedWithOrganization: function () {
|
||||
return this.acl.any(this._hasTypeOrg);
|
||||
},
|
||||
|
||||
clone: function () {
|
||||
var attrs = _.clone(this.attributes);
|
||||
delete attrs.id;
|
||||
return new PermissionModel(attrs, { configModel: this._configModel });
|
||||
},
|
||||
|
||||
/**
|
||||
* Overwrite this ACL list from other permission object
|
||||
* @param otherPermission {Object} instance of PermissionModel
|
||||
*/
|
||||
overwriteAcl: function (otherPermission) {
|
||||
this.acl.reset(otherPermission.acl.models);
|
||||
},
|
||||
|
||||
// Note that this may return an inherited ACL item
|
||||
// use ._ownAclItem instead if only model's own is wanted (if there is any)
|
||||
findRepresentableAclItem: function (model) {
|
||||
if (this.isOwner(model)) {
|
||||
return this._newAclItem(model, this.constructor.READ_WRITE);
|
||||
} else {
|
||||
var checkList = ['_ownAclItem', '_organizationAclItem', '_mostPrivilegedGroupAclItem'];
|
||||
return this._findMostPrivilegedAclItem(checkList, function (fnName) {
|
||||
return this[fnName](model);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_hasTypeUser: function (m) {
|
||||
return m.get('type') === 'user';
|
||||
},
|
||||
|
||||
_getEntity: function (m) {
|
||||
return m.get('entity');
|
||||
},
|
||||
|
||||
_hasTypeOrg: function (m) {
|
||||
return m.get('type') === 'org';
|
||||
},
|
||||
|
||||
_isOrganization: function (object) {
|
||||
return object instanceof OrganizationModel;
|
||||
},
|
||||
|
||||
_ownAclItem: function (model) {
|
||||
if (!model || !_.isFunction(model.isNew)) {
|
||||
console.error('model is required to find an ACL item');
|
||||
}
|
||||
if (!model.isNew()) {
|
||||
return this.acl.find(function (aclItem) {
|
||||
return aclItem.get('entity').id === model.id;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_organizationAclItem: function (m) {
|
||||
var org = _.result(m.collection, 'organization') || m.organization;
|
||||
if (org) {
|
||||
return this._ownAclItem(org);
|
||||
}
|
||||
},
|
||||
|
||||
_mostPrivilegedGroupAclItem: function (m) {
|
||||
var groups = _.result(m.groups, 'models');
|
||||
if (groups) {
|
||||
return this._findMostPrivilegedAclItem(groups, this._ownAclItem);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterates over a items in given list using the iteratee, stops and returns when found the ACL item with best access (i.e. READ_WRITE), or the
|
||||
* list is completed.
|
||||
* @param {Array} list
|
||||
* @param {Function} iteratee that takes an item from list and returns an access
|
||||
* iteratee is called in context of this model.
|
||||
* @Return {String} 'r', 'rw', or undefined if there were no access for given item
|
||||
*/
|
||||
_findMostPrivilegedAclItem: function (list, iteratee) {
|
||||
var aclItem;
|
||||
for (var i = 0, x = list[i]; x && result(aclItem, 'get', 'access') !== PermissionModel.READ_WRITE; x = list[++i]) {
|
||||
// Keep last ACL item if iteratee returns nothing
|
||||
aclItem = iteratee.call(this, x) || aclItem;
|
||||
}
|
||||
return aclItem;
|
||||
},
|
||||
|
||||
/**
|
||||
* Grants access to a set of items
|
||||
* @param {Object} model
|
||||
* @param {String} access can take the following values:
|
||||
* - 'r': read only
|
||||
* - 'rw': read and write permission
|
||||
*/
|
||||
_grantAccess: function (model, access) {
|
||||
var aclItem = this._ownAclItem(model);
|
||||
if (aclItem) {
|
||||
aclItem.set('access', access);
|
||||
} else {
|
||||
aclItem = this._newAclItem(model, access);
|
||||
if (aclItem.isValid()) {
|
||||
this.acl.add(aclItem);
|
||||
} else {
|
||||
throw new Error(access + ' is not a valid ACL access');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_newAclItem: function (model, access) {
|
||||
const type = model.get('type') || model.getModelType();
|
||||
|
||||
return new ACLItemModel({
|
||||
type: type,
|
||||
entity: model,
|
||||
access: access
|
||||
});
|
||||
}
|
||||
|
||||
}, {
|
||||
|
||||
READ_ONLY: 'r',
|
||||
READ_WRITE: 'rw'
|
||||
|
||||
});
|
||||
|
||||
module.exports = PermissionModel;
|
||||
@@ -0,0 +1,15 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Invalidate service token
|
||||
*
|
||||
* - It needs a datasource name or it won't work.
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
idAttribute: 'datasource',
|
||||
|
||||
url: function () {
|
||||
return `/api/v1/imports/service/${this.get(this.idAttribute)}/invalidate_token`;
|
||||
}
|
||||
});
|
||||
32
lib/assets/javascripts/dashboard/data/service-oauth-model.js
Normal file
32
lib/assets/javascripts/dashboard/data/service-oauth-model.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'datasourceName',
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get oauth url from the service requested
|
||||
*
|
||||
* - It needs a datasource name or it won't work.
|
||||
*
|
||||
* new ServiceOauthModel({ datasourceName: 'dropbox', configModel })
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
_datasourceName: 'dropbox',
|
||||
|
||||
initialize: function (attributes, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
const version = this._configModel.urlVersion('imports_service', method);
|
||||
return `/api/${version}/imports/service/${this._datasourceName}/auth_url`;
|
||||
},
|
||||
|
||||
parse: function (response) {
|
||||
return response;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if service token is valid
|
||||
*
|
||||
* - It needs a datasource name or it won't work.
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
idAttribute: 'datasource',
|
||||
|
||||
initialize: function (attributes, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
const version = this._configModel.urlVersion('imports_service', method);
|
||||
return `/api/${version}/imports/service/${this.get(this.idAttribute)}/token_valid`;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
// TODO: Maybe this file is unnecessary
|
||||
|
||||
const SlideTransition = Backbone.Model.extend({
|
||||
defaults: {
|
||||
time: 0
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SlideTransition;
|
||||
@@ -0,0 +1,141 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Synced table model
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
_X: 1.2, // Multiply current interval for this number
|
||||
_INTERVAL: 1500, // Interval time between poll checkings
|
||||
_STATES: ['created', 'failure', 'success', 'syncing', 'queued'],
|
||||
|
||||
defaults: {
|
||||
name: '',
|
||||
url: '',
|
||||
state: '',
|
||||
run_at: 0,
|
||||
ran_at: 0,
|
||||
retried_times: 0,
|
||||
interval: 0,
|
||||
error_code: 0,
|
||||
error_message: '',
|
||||
service_name: '',
|
||||
service_item_id: '',
|
||||
content_guessing: true,
|
||||
type_guessing: true
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('synchronization', method);
|
||||
|
||||
var base = '/api/' + version + '/synchronizations/';
|
||||
if (this.isNew()) {
|
||||
return base;
|
||||
}
|
||||
return base + this.id;
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
this.bind('destroy', function () {
|
||||
this.unset('id');
|
||||
});
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
var c = _.clone(this.attributes);
|
||||
|
||||
var d = {
|
||||
url: c.url,
|
||||
interval: c.interval,
|
||||
content_guessing: c.content_guessing,
|
||||
type_guessing: c.type_guessing,
|
||||
create_vis: c.create_vis
|
||||
};
|
||||
|
||||
if (c.type === 'remote') {
|
||||
_.extend(d, {
|
||||
remote_visualization_id: c.remote_visualization_id,
|
||||
create_vis: false,
|
||||
value: c.value
|
||||
});
|
||||
}
|
||||
|
||||
if (c.id !== undefined) {
|
||||
d.id = c.id;
|
||||
}
|
||||
|
||||
// Comes from a service?
|
||||
if (c.service_name) {
|
||||
d.service_name = c.service_name;
|
||||
d.service_item_id = c.service_item_id;
|
||||
}
|
||||
|
||||
return d;
|
||||
},
|
||||
|
||||
syncNow: function (callback) {
|
||||
$.ajax({
|
||||
url: this._configModel.prefixUrl() + this.url() + '/sync_now',
|
||||
type: 'PUT'
|
||||
}).always(callback);
|
||||
},
|
||||
|
||||
// Checks for poll to finish
|
||||
pollCheck: function (i) {
|
||||
var self = this;
|
||||
var interval = this._INTERVAL;
|
||||
|
||||
this.pollTimer = setInterval(request, interval);
|
||||
|
||||
function request () {
|
||||
self.destroyCheck();
|
||||
|
||||
self.fetch({
|
||||
error: function (m, e) {
|
||||
self.set({
|
||||
error_message: e.statusText || '',
|
||||
state: 'failure'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
interval = interval * self._X;
|
||||
|
||||
self.pollTimer = setInterval(request, interval);
|
||||
}
|
||||
},
|
||||
|
||||
destroyCheck: function () {
|
||||
clearInterval(this.pollTimer);
|
||||
},
|
||||
|
||||
isSync: function () {
|
||||
return !this.isNew();
|
||||
},
|
||||
|
||||
linkToTable: function (table) {
|
||||
var self = this;
|
||||
if (table.has('synchronization')) {
|
||||
this.set(table.get('synchronization'));
|
||||
}
|
||||
|
||||
table.bind('change:synchronization', function () {
|
||||
self.set(table.get('synchronization'));
|
||||
}, table);
|
||||
|
||||
table.bind('destroy', function destroy () {
|
||||
self.unbind(null, null, table);
|
||||
self.destroy();
|
||||
}, table);
|
||||
// TODO: manage table renaming
|
||||
}
|
||||
|
||||
});
|
||||
813
lib/assets/javascripts/dashboard/data/table/carto-table-data.js
Normal file
813
lib/assets/javascripts/dashboard/data/table/carto-table-data.js
Normal file
@@ -0,0 +1,813 @@
|
||||
const $ = require('jquery');
|
||||
const Backbone = require('backbone');
|
||||
const _ = require('underscore');
|
||||
const TableDataCollection = require('dashboard/data/table/table-data-collection');
|
||||
const RowModel = require('dashboard/data/table/row-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const WKT = require('dashboard/common/wkt');
|
||||
const safeTableNameQuoting = require('dashboard/helpers/safe-table-name-quoting');
|
||||
const cartoMetadataStatic = require('dashboard/views/public-dataset/carto-table-metadata-static');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
module.exports = TableDataCollection.extend({
|
||||
_ADDED_ROW_TEXT: 'Row added correctly',
|
||||
_ADDING_ROW_TEXT: 'Adding a new row',
|
||||
_GEOMETRY_UPDATED: 'Table geometry updated',
|
||||
|
||||
model: function (attrs, opts) {
|
||||
var configModel = opts.collection._configModel;
|
||||
return new RowModel(attrs, {
|
||||
configModel,
|
||||
// TODO: Check this
|
||||
collection: opts.collection
|
||||
});
|
||||
},
|
||||
|
||||
initialize: function (models, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this.table = options ? options.table : null;
|
||||
this.model.prototype.idAttribute = 'cartodb_id';
|
||||
this.initOptions();
|
||||
this.filter = null;
|
||||
this._fetching = false;
|
||||
this.pages = [];
|
||||
this.lastPage = false;
|
||||
this.bind('newPage', this.newPage, this);
|
||||
this.bind('reset', function () {
|
||||
var pages = Math.floor(this.size() / this.options.get('rows_per_page'));
|
||||
this.pages = [];
|
||||
|
||||
for (var i = 0; i < pages; ++i) {
|
||||
this.pages.push(i);
|
||||
}
|
||||
}, this);
|
||||
|
||||
if (this.table) {
|
||||
this.bind('add change:the_geom', function (row) {
|
||||
var gt = this.table.get('geometry_types');
|
||||
if (gt && gt.length > 0) return;
|
||||
if (row.get('the_geom')) {
|
||||
// we set it to silent because a change in geometry_types
|
||||
// raises rendering and column feching
|
||||
this.table.addGeomColumnType(row.getGeomType());
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
TableDataCollection.prototype.initialize.call(this);
|
||||
},
|
||||
|
||||
initOptions: function () {
|
||||
this.options = new Backbone.Model({
|
||||
rows_per_page: 40,
|
||||
page: 0,
|
||||
sort_order: 'asc',
|
||||
order_by: 'cartodb_id',
|
||||
filter_column: '',
|
||||
filter_value: ''
|
||||
});
|
||||
this.options.bind('change', () => {
|
||||
if (this._fetching) {
|
||||
return;
|
||||
}
|
||||
this._fetching = true;
|
||||
var opt = {};
|
||||
var previous = this.options.previous('page');
|
||||
|
||||
if (this.options.hasChanged('page')) {
|
||||
opt.add = true;
|
||||
opt.changingPage = true;
|
||||
// if user is going backwards insert new rows at the top
|
||||
if (previous > this.options.get('page')) {
|
||||
opt.at = 0;
|
||||
}
|
||||
} else {
|
||||
if (this.options.hasChanged('mode')) {
|
||||
this.options.set({
|
||||
'page': 0
|
||||
}, { silent: true });
|
||||
}
|
||||
}
|
||||
|
||||
opt.success = (_coll, resp) => {
|
||||
this.trigger('loaded');
|
||||
if (resp.rows && resp.rows.length !== 0) {
|
||||
if (opt.changingPage) {
|
||||
this.trigger('newPage', this.options.get('page'), opt.at === 0 ? 'up' : 'down');
|
||||
}
|
||||
} else {
|
||||
// no data so do not change the page
|
||||
this.options.set({page: previous});//, { silent: true });
|
||||
}
|
||||
this.trigger('endLoadingRows', this.options.get('page'), opt.at === 0 ? 'up' : 'down');
|
||||
this._fetching = false;
|
||||
};
|
||||
|
||||
opt.error = () => {
|
||||
console.error('there was some problem fetching rows');
|
||||
this.trigger('endLoadingRows');
|
||||
this._fetching = false;
|
||||
};
|
||||
|
||||
this.trigger('loadingRows', opt.at === 0 ? 'up' : 'down');
|
||||
|
||||
this.fetch(opt);
|
||||
}, this);
|
||||
},
|
||||
|
||||
parse: function (d) {
|
||||
// when the query modifies the data modified flag is true
|
||||
// TODO: change this when SQL API was able to say if a
|
||||
// query modify some data
|
||||
// HACK, it will fail if using returning sql statement
|
||||
this.modify_rows = d.rows.length === 0 && _.size(d.fields) === 0;
|
||||
this.affected_rows = d.affected_rows;
|
||||
this.lastPage = false;
|
||||
if (d.rows.length < this.options.get('rows_per_page')) {
|
||||
this.lastPage = true;
|
||||
}
|
||||
return d.rows;
|
||||
},
|
||||
|
||||
// given fields array as they come from SQL create a map name -> type
|
||||
_schemaFromQueryFields: function (fields) {
|
||||
var sc = {};
|
||||
for (var k in fields) {
|
||||
sc[k] = fields[k].type;
|
||||
}
|
||||
return sc;
|
||||
},
|
||||
|
||||
_createUrlOptions: function (filter) {
|
||||
var attr;
|
||||
if (filter) {
|
||||
var a = {};
|
||||
for (var k in this.options.attributes) {
|
||||
if (filter(k)) {
|
||||
a[k] = this.options.attributes[k];
|
||||
}
|
||||
}
|
||||
attr = _(a);
|
||||
} else {
|
||||
attr = _(this.options.attributes);
|
||||
}
|
||||
var params = attr.map(function (v, k) {
|
||||
return k + '=' + encodeURIComponent(v);
|
||||
}).join('&');
|
||||
params += '&api_key=' + this._configModel.get('api_key');
|
||||
return params;
|
||||
},
|
||||
|
||||
_geometryColumnSQL: function (c) {
|
||||
return [
|
||||
'CASE',
|
||||
'WHEN GeometryType(' + c + ") = 'POINT' THEN",
|
||||
'ST_AsGeoJSON(' + c + ',8)',
|
||||
'WHEN (' + c + ' IS NULL) THEN',
|
||||
'NULL',
|
||||
'ELSE',
|
||||
'GeometryType(' + c + ')',
|
||||
'END ' + c
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
// return wrapped SQL removing the_geom and the_geom_webmercator
|
||||
// to avoid fetching those columns.
|
||||
// So for a sql like
|
||||
// select * from table the returned value is
|
||||
// select column1, column2, column3... from table
|
||||
wrappedSQL: function (schema, exclude, fetchGeometry) {
|
||||
exclude = exclude || ['the_geom_webmercator'];
|
||||
schema = _.clone(schema);
|
||||
|
||||
var select_columns = _.chain(schema).omit(exclude).map((v, k) => {
|
||||
if (v === 'geometry') {
|
||||
if (fetchGeometry) {
|
||||
return 'st_astext("' + k + '") ' + 'as ' + k;
|
||||
}
|
||||
return this._geometryColumnSQL(k);
|
||||
}
|
||||
return '"' + k + '"';
|
||||
}).value();
|
||||
|
||||
select_columns = select_columns.join(',');
|
||||
|
||||
var mode = this.options.get('sort_order') === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
var q = 'select ' + select_columns + ' from (' + this.getSQL() + ') __wrapped';
|
||||
var order_by = this.options.get('order_by');
|
||||
if (order_by && order_by.length > 0) {
|
||||
q += ' order by ' + order_by + ' ' + mode;
|
||||
}
|
||||
return q;
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return this.sqlApiUrl();
|
||||
},
|
||||
|
||||
/**
|
||||
* we need to override sync to avoid the sql request to be sent by GET.
|
||||
* For security reasons, we need them to be send as a PUT request.
|
||||
* @method sync
|
||||
* @param method {'save' || 'read' || 'delete' || 'create'}
|
||||
* @param model {Object}
|
||||
* @param options {Object}
|
||||
*/
|
||||
sync: function (method, model, options) {
|
||||
if (!options) { options = {}; }
|
||||
options.data = this._createUrlOptions(function (d) {
|
||||
return d !== 'sql';
|
||||
});
|
||||
|
||||
if (cartoMetadataStatic.alterTableData(this.options.get('sql') || '')) {
|
||||
options.data += '&q=' + encodeURIComponent(this.options.get('sql'));
|
||||
options.type = 'POST';
|
||||
} else {
|
||||
// when a geometry can be lazy fetched, don't fetch it
|
||||
var fetchGeometry = 'cartodb_id' in this.query_schema;
|
||||
options.data += '&q=' + encodeURIComponent(this.wrappedSQL(this.query_schema, [], !fetchGeometry));
|
||||
|
||||
if (options.data.length > 2048) {
|
||||
options.type = 'POST';
|
||||
}
|
||||
}
|
||||
|
||||
return Backbone.sync.call(this, method, this, options);
|
||||
},
|
||||
|
||||
sqlApiUrl: function () {
|
||||
return this._configModel.getSqlApiUrl();
|
||||
},
|
||||
|
||||
setOptions: function (opt) {
|
||||
this.options.set(opt);
|
||||
},
|
||||
|
||||
// Refresh all table data
|
||||
refresh: function () {
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
isFetchingPage: function () {
|
||||
return this._fetching;
|
||||
},
|
||||
|
||||
loadPageAtTop: function () {
|
||||
if (!this._fetching) {
|
||||
var first = this.pages[0];
|
||||
|
||||
if (first > 0) {
|
||||
this.options.set('page', first - 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
loadPageAtBottom: function () {
|
||||
if (!this._fetching) {
|
||||
var last = this.pages[this.pages.length - 1];
|
||||
|
||||
if (!this.lastPage) {
|
||||
this.options.set('page', last + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* called when a new page is loaded
|
||||
* removes the models to max
|
||||
*/
|
||||
newPage: function (currentPage, direction) {
|
||||
if (this.pages.indexOf(currentPage) < 0) {
|
||||
this.pages.push(currentPage);
|
||||
}
|
||||
this.pages.sort(function (a, b) {
|
||||
return Number(a) > Number(b);
|
||||
});
|
||||
// remove blocks if there are more rows than allowed
|
||||
var rowspp = this.options.get('rows_per_page');
|
||||
var max_items = rowspp * 4;
|
||||
if (this.size() > max_items) {
|
||||
if (direction == 'up') { // eslint-disable-line eqeqeq
|
||||
// remove page from the bottom (the user is going up)
|
||||
this.pages.pop();
|
||||
this.remove(this.models.slice(max_items, this.size()));
|
||||
} else {
|
||||
// remove page from the top (the user is going down)
|
||||
this.pages.shift();
|
||||
this.remove(this.models.slice(0, rowspp));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addRow: function (opts) {
|
||||
this.table.notice(this._ADDING_ROW_TEXT, 'load', 0);
|
||||
opts = opts || {};
|
||||
_.extend(opts, {
|
||||
wait: true,
|
||||
success: () => {
|
||||
this.table.notice(this._ADDED_ROW_TEXT);
|
||||
},
|
||||
error: (e, resp) => {
|
||||
// TODO: notice user
|
||||
this.table.error(this._ADDING_ROW_TEXT, resp);
|
||||
}
|
||||
});
|
||||
return this.create(null, opts);
|
||||
},
|
||||
|
||||
/**
|
||||
* creates a new row model in local, it is NOT serialized to the server
|
||||
*/
|
||||
newRow: function (attrs) {
|
||||
var r = new RowModel(attrs, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
r.table = this.table;
|
||||
r.bind('saved', function _saved () {
|
||||
if (r.table.data().length === 0) {
|
||||
r.table.data().fetch();
|
||||
r.unbind('saved', _saved, r.table);
|
||||
}
|
||||
}, r.table);
|
||||
return r;
|
||||
},
|
||||
|
||||
/**
|
||||
* return a model row
|
||||
*/
|
||||
getRow: function (id, options) {
|
||||
options = options || {};
|
||||
var r = this.get(id);
|
||||
if (!r) {
|
||||
r = new RowModel({cartodb_id: id}, { configModel: this._configModel });
|
||||
}
|
||||
if (!options.no_add) {
|
||||
this.table._data.add(r);
|
||||
}
|
||||
r.table = this.table;
|
||||
return r;
|
||||
},
|
||||
|
||||
getRowAt: function (index) {
|
||||
var r = this.at(index);
|
||||
r.table = this.table;
|
||||
return r;
|
||||
},
|
||||
|
||||
deleteRow: function (row_id) {
|
||||
},
|
||||
|
||||
isReadOnly: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
quartiles: function (nslots, column, callback, error) {
|
||||
var tmpl = _.template('select quartile, max(<%= column %>) as maxamount from (select <%= column %>, ntile(<%= slots %>) over (order by <%= column %>) as quartile from (<%= sql %>) _table_sql where <%= column %> is not null) x group by quartile order by quartile');
|
||||
this._sqlQuery(tmpl({
|
||||
slots: nslots,
|
||||
sql: this.getSQL(),
|
||||
column: column
|
||||
}),
|
||||
function (data) {
|
||||
callback(_(data.rows).pluck('maxamount'));
|
||||
},
|
||||
error);
|
||||
},
|
||||
|
||||
equalInterval: function (nslots, column, callback, error) {
|
||||
var tmpl = _.template(`
|
||||
with params as (select min(a), max(a) from ( select <%= column %> as a from (<%= sql %>) _table_sql where <%= column %> is not null ) as foo )
|
||||
select (max-min)/<%= slots %> as s, min, max from params`
|
||||
);
|
||||
this._sqlQuery(tmpl({
|
||||
slots: nslots,
|
||||
sql: this.getSQL(),
|
||||
column: column
|
||||
}),
|
||||
function (data) {
|
||||
var min = data.rows[0].min;
|
||||
var max = data.rows[0].max;
|
||||
var range = data.rows[0].s;
|
||||
var values = [];
|
||||
|
||||
for (var i = 1, l = nslots; i < l; i++) {
|
||||
values.push((range * i) + min);
|
||||
}
|
||||
|
||||
// Add last value
|
||||
values.push(max);
|
||||
// Callback
|
||||
callback(values);
|
||||
},
|
||||
error);
|
||||
},
|
||||
|
||||
_quantificationMethod: function (functionName, nslots, column, distinct, callback, error) {
|
||||
var tmpl = _.template('select unnest(<%= functionName %>(array_agg(<%= simplify_fn %>((<%= column %>::numeric))), <%= slots %>)) as buckets from (<%= sql %>) _table_sql where <%= column %> is not null');
|
||||
this._sqlQuery(tmpl({
|
||||
slots: nslots,
|
||||
sql: this.getSQL(),
|
||||
column: column,
|
||||
functionName: functionName,
|
||||
simplify_fn: 'distinct'
|
||||
}),
|
||||
function (data) {
|
||||
callback(_(data.rows).pluck('buckets'));
|
||||
},
|
||||
error);
|
||||
},
|
||||
|
||||
discreteHistogram: function (nbuckets, column, callback, error) {
|
||||
var query = 'SELECT DISTINCT(<%= column %>) AS bucket, count(*) AS value FROM (<%= sql %>) _table_sql GROUP BY <%= column %> ORDER BY value DESC LIMIT <%= nbuckets %> + 1';
|
||||
|
||||
var sql = _.template(query, {
|
||||
column: column,
|
||||
nbuckets: nbuckets,
|
||||
sql: this.getSQL()
|
||||
});
|
||||
|
||||
this._sqlQuery(sql, function (data) {
|
||||
var count = data.rows.length;
|
||||
var reached_limit = false;
|
||||
|
||||
if (count > nbuckets) {
|
||||
data.rows = data.rows.slice(0, nbuckets);
|
||||
reached_limit = true;
|
||||
}
|
||||
|
||||
callback({ rows: data.rows, reached_limit: reached_limit }); // eslint-disable-line
|
||||
});
|
||||
},
|
||||
|
||||
date_histogram: function (nbuckets, column, callback, error) {
|
||||
column = 'EXTRACT(EPOCH FROM ' + column + '::TIMESTAMP WITH TIME ZONE )';
|
||||
|
||||
var tmpl = _.template(
|
||||
'with bounds as ( ' +
|
||||
'SELECT ' +
|
||||
'current_timestamp as tz, ' +
|
||||
'min(<%= column %>) as lower, ' +
|
||||
'max(<%= column %>) as upper, ' +
|
||||
'(max(<%= column %>) - min(<%= column %>)) as span, ' +
|
||||
'CASE WHEN ABS((max(<%= column %>) - min(<%= column %>))/<%= nbuckets %>) <= 0 THEN 1 ELSE GREATEST(1.0, pow(10,ceil(log((max(<%= column %>) - min(<%= column %>))/<%= nbuckets %>)))) END as bucket_size ' +
|
||||
'FROM (<%= sql %>) _table_sql ' +
|
||||
') ' +
|
||||
'select array_agg(v) val, array_agg(bucket) buckets, tz, bounds.upper, bounds.lower, bounds.span, bounds.bucket_size from ' +
|
||||
'( ' +
|
||||
'select ' +
|
||||
'count(<%= column %>) as v, ' +
|
||||
'round((<%= column %> - bounds.lower)/bounds.bucket_size) as bucket ' +
|
||||
'from (<%= sql %>) _table_sql, bounds ' +
|
||||
'where <%= column %> is not null ' +
|
||||
'group by bucket order by bucket ' +
|
||||
') a, bounds ' +
|
||||
'group by ' +
|
||||
'bounds.upper, bounds.lower, bounds.span, bounds.bucket_size, bounds.tz ');
|
||||
|
||||
// transform array_agg from postgres to a js array
|
||||
function agg_array (a) {
|
||||
return a.map(function (v) { return parseFloat(v); });
|
||||
}
|
||||
|
||||
this._sqlQuery(tmpl({
|
||||
nbuckets: nbuckets,
|
||||
sql: this.getSQL(),
|
||||
column: column
|
||||
}),
|
||||
|
||||
function (data) {
|
||||
if (!data.rows || data.rows.length === 0) {
|
||||
callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
data = data.rows[0];
|
||||
data.val = agg_array(data.val);
|
||||
data.buckets = agg_array(data.buckets);
|
||||
|
||||
var hist = [];
|
||||
var bounds = {};
|
||||
|
||||
// create a sorted array and normalize
|
||||
var upper = data.upper;
|
||||
var lower = data.lower;
|
||||
var tz = data.tz;
|
||||
var bucket_size = data.bucket_size;
|
||||
var max;
|
||||
|
||||
max = data.val[0];
|
||||
|
||||
for (var r = 0; r < data.buckets.length; ++r) {
|
||||
var b = data.buckets[r];
|
||||
var v = hist[b] = data.val[r];
|
||||
max = Math.max(max, v);
|
||||
}
|
||||
|
||||
// var maxBucket = _.max(data.buckets)
|
||||
for (var i = 0; i < hist.length; ++i) {
|
||||
if (hist[i] === undefined) {
|
||||
hist[i] = 0;
|
||||
} else {
|
||||
hist[i] = hist[i] / max;
|
||||
}
|
||||
}
|
||||
|
||||
bounds.upper = parseFloat(upper);
|
||||
bounds.lower = parseFloat(lower);
|
||||
bounds.bucket_size = parseFloat(bucket_size);
|
||||
bounds.tz = tz;
|
||||
|
||||
callback(hist, bounds);
|
||||
},
|
||||
|
||||
error);
|
||||
},
|
||||
|
||||
histogram: function (nbuckets, column, callback, error) {
|
||||
var tmpl = _.template(
|
||||
'with bounds as ( ' +
|
||||
'SELECT ' +
|
||||
'min(<%= column %>) as lower, ' +
|
||||
'max(<%= column %>) as upper, ' +
|
||||
'(max(<%= column %>) - min(<%= column %>)) as span, ' +
|
||||
'CASE WHEN ABS((max(<%= column %>) - min(<%= column %>))/<%= nbuckets %>) <= 0 THEN 1 ELSE GREATEST(1.0, pow(10,ceil(log((max(<%= column %>) - min(<%= column %>))/<%= nbuckets %>)))) END as bucket_size ' +
|
||||
'FROM (<%= sql %>) _table_sql ' +
|
||||
') ' +
|
||||
'select array_agg(v) val, array_agg(bucket) buckets, bounds.upper, bounds.lower, bounds.span, bounds.bucket_size from ' +
|
||||
'( ' +
|
||||
'select ' +
|
||||
'count(<%= column %>) as v, ' +
|
||||
'round((<%= column %> - bounds.lower)/bounds.bucket_size) as bucket ' +
|
||||
'from (<%= sql %>) _table_sql, bounds ' +
|
||||
'where <%= column %> is not null ' +
|
||||
'group by bucket order by bucket ' +
|
||||
') a, bounds ' +
|
||||
'group by ' +
|
||||
'bounds.upper, ' +
|
||||
'bounds.lower, bounds.span, bounds.bucket_size ');
|
||||
|
||||
// transform array_agg from postgres to a js array
|
||||
function agg_array (a) {
|
||||
return a.map(function (v) { return parseFloat(v); });
|
||||
// return JSON.parse(a.replace('{', '[').replace('}', ']'))
|
||||
}
|
||||
|
||||
this._sqlQuery(tmpl({
|
||||
nbuckets: nbuckets,
|
||||
sql: this.getSQL(),
|
||||
column: column
|
||||
}),
|
||||
|
||||
function (data) {
|
||||
if (!data.rows || data.rows.length === 0) {
|
||||
callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
data = data.rows[0];
|
||||
|
||||
data.val = agg_array(data.val);
|
||||
data.buckets = agg_array(data.buckets);
|
||||
|
||||
var hist = [];
|
||||
var bounds = {};
|
||||
|
||||
// create a sorted array and normalize
|
||||
var upper = data.upper;
|
||||
var lower = data.lower;
|
||||
var bucket_size = data.bucket_size;
|
||||
var max;
|
||||
|
||||
max = data.val[0];
|
||||
|
||||
for (var r = 0; r < data.buckets.length; ++r) {
|
||||
var b = data.buckets[r];
|
||||
var v = hist[b] = data.val[r];
|
||||
max = Math.max(max, v);
|
||||
}
|
||||
|
||||
// var maxBucket = _.max(data.buckets)
|
||||
for (var i = 0; i < hist.length; ++i) {
|
||||
if (hist[i] === undefined) {
|
||||
hist[i] = 0;
|
||||
} else {
|
||||
hist[i] = hist[i] / max;
|
||||
}
|
||||
}
|
||||
|
||||
bounds.upper = parseFloat(upper);
|
||||
bounds.lower = parseFloat(lower);
|
||||
bounds.bucket_size = parseFloat(bucket_size);
|
||||
|
||||
callback(hist, bounds);
|
||||
},
|
||||
|
||||
error);
|
||||
},
|
||||
|
||||
jenkBins: function (nslots, column, callback, error) {
|
||||
this._quantificationMethod('CDB_JenksBins', nslots, column, true, callback, error);
|
||||
},
|
||||
|
||||
headTails: function (nslots, column, callback, error) {
|
||||
this._quantificationMethod('CDB_HeadsTailsBins', nslots, column, false, callback, error);
|
||||
},
|
||||
|
||||
quantileBins: function (nslots, column, callback, error) {
|
||||
this._quantificationMethod('CDB_QuantileBins', nslots, column, false, callback, error);
|
||||
},
|
||||
|
||||
categoriesForColumn: function (max_values, column, callback, error) {
|
||||
var tmpl = _.template('SELECT <%= column %>, count(<%= column %>) FROM (<%= sql %>) _table_sql ' +
|
||||
'GROUP BY <%= column %> ORDER BY count DESC LIMIT <%= max_values %> '
|
||||
);
|
||||
|
||||
this._sqlQuery(tmpl({
|
||||
sql: this.getSQL(),
|
||||
column: column,
|
||||
max_values: max_values + 1
|
||||
}),
|
||||
function (data) {
|
||||
callback({// eslint-disable-line
|
||||
type: data.fields[column].type || 'string',
|
||||
categories: _(data.rows).pluck(column)
|
||||
});
|
||||
},
|
||||
error);
|
||||
},
|
||||
|
||||
/**
|
||||
* call callback with the geometry bounds
|
||||
*/
|
||||
geometryBounds: function (callback) {
|
||||
var tmpl = _.template('SELECT ST_XMin(ST_Extent(the_geom)) as minx,ST_YMin(ST_Extent(the_geom)) as miny, ST_XMax(ST_Extent(the_geom)) as maxx,ST_YMax(ST_Extent(the_geom)) as maxy from (<%= sql %>) _table_sql');
|
||||
this._sqlQuery(tmpl({
|
||||
sql: this.getSQL()
|
||||
}),
|
||||
function (result) {
|
||||
var coordinates = result.rows[0];
|
||||
|
||||
var lon0 = coordinates.maxx;
|
||||
var lat0 = coordinates.maxy;
|
||||
var lon1 = coordinates.minx;
|
||||
var lat1 = coordinates.miny;
|
||||
|
||||
var minlat = -85.0511;
|
||||
var maxlat = 85.0511;
|
||||
var minlon = -179;
|
||||
var maxlon = 179;
|
||||
|
||||
var clampNum = function (x, min, max) {
|
||||
return x < min ? min : x > max ? max : x;
|
||||
};
|
||||
|
||||
lon0 = clampNum(lon0, minlon, maxlon);
|
||||
lon1 = clampNum(lon1, minlon, maxlon);
|
||||
lat0 = clampNum(lat0, minlat, maxlat);
|
||||
lat1 = clampNum(lat1, minlat, maxlat);
|
||||
callback([ [lat0, lon0], [lat1, lon1]]); // eslint-disable-line
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_sqlQuery: function (sql, callback, error, type) {
|
||||
var s = encodeURIComponent(sql);
|
||||
return $.ajax({
|
||||
type: type || 'POST',
|
||||
data: 'q=' + s + '&api_key=' + this._configModel.get('api_key'),
|
||||
url: this.url(),
|
||||
success: callback,
|
||||
error: error
|
||||
});
|
||||
},
|
||||
|
||||
getSQL: function () {
|
||||
// use table.id to fetch data because if always contains the real table name
|
||||
return 'select * from ' + safeTableNameQuoting(this.table.get('id'));
|
||||
},
|
||||
|
||||
fetch: function (opts) {
|
||||
opts = opts || {};
|
||||
if (!opts || !opts.add) {
|
||||
this.options.attributes.page = 0;
|
||||
this.options._previousAttributes.page = 0;
|
||||
this.pages = [];
|
||||
}
|
||||
var error = opts.error;
|
||||
opts.error = (model, resp) => {
|
||||
this.fetched = true;
|
||||
this.trigger('error', model, resp);
|
||||
error && error(model, resp);
|
||||
};
|
||||
var success = opts.success;
|
||||
opts.success = (model, resp) => {
|
||||
this.fetched = true;
|
||||
success && success.apply(this, arguments);
|
||||
};
|
||||
this._fetch(opts);
|
||||
},
|
||||
|
||||
_fetch: function (opts) {
|
||||
var MAX_GET_LENGTH = 1024;
|
||||
this.trigger('loading', opts);
|
||||
|
||||
var sql = this.getSQL();
|
||||
// if the query changes the database just send it
|
||||
if (cartoMetadataStatic.alterTableData(sql)) {
|
||||
TableDataCollection.prototype.fetch.call(this, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
// use get to fetch the schema, probably cached
|
||||
this._sqlQuery(_.template('select * from (<%= sql %>) __wrapped limit 0')({ sql: sql }), (data) => {
|
||||
// get schema
|
||||
this.query_schema = this._schemaFromQueryFields(data.fields);
|
||||
if (!this.table.isInSQLView()) {
|
||||
if ('the_geom' in this.query_schema) {
|
||||
delete this.query_schema['the_geom_webmercator'];
|
||||
}
|
||||
}
|
||||
TableDataCollection.prototype.fetch.call(this, opts);
|
||||
}, (err) => {
|
||||
this.trigger('error', this, err);
|
||||
}, sql.length > MAX_GET_LENGTH ? 'POST' : 'GET');
|
||||
},
|
||||
|
||||
/**
|
||||
* with the data from the rows fetch create an schema
|
||||
* if the schema from original table is passed the method
|
||||
* set the column types according to it
|
||||
* return an empty list if no data was fetch
|
||||
*/
|
||||
schemaFromData: function (originalTableSchema) {
|
||||
// build schema in format [ [field, type] , ...]
|
||||
return cartoMetadataStatic.sortSchema(_(this.query_schema).map(function (v, k) {
|
||||
return [k, v];
|
||||
}));
|
||||
},
|
||||
|
||||
geometryTypeFromGeoJSON: function (geojson) {
|
||||
try {
|
||||
var geo = JSON.parse(geojson);
|
||||
return geo.type;
|
||||
} catch (e) {
|
||||
}
|
||||
},
|
||||
|
||||
geometryTypeFromWKT: function (wkt) {
|
||||
if (!wkt) return null;
|
||||
var types = WKT.types;
|
||||
wkt = wkt.toUpperCase();
|
||||
for (var i = 0; i < types.length; ++i) {
|
||||
var t = types[i];
|
||||
if (wkt.indexOf(t) !== -1) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
geometryTypeFromWKB: function (wkb) {
|
||||
if (!wkb) return null;
|
||||
|
||||
var typeMap = {
|
||||
'0001': 'Point',
|
||||
'0002': 'LineString',
|
||||
'0003': 'Polygon',
|
||||
'0004': 'MultiPoint',
|
||||
'0005': 'MultiLineString',
|
||||
'0006': 'MultiPolygon'
|
||||
};
|
||||
|
||||
var bigendian = wkb[0] === '0' && wkb[1] === '0';
|
||||
var type = wkb.substring(2, 6);
|
||||
if (!bigendian) {
|
||||
// swap '0100' => '0001'
|
||||
type = type[2] + type[3] + type[0] + type[1];
|
||||
}
|
||||
return typeMap[type];
|
||||
},
|
||||
|
||||
//
|
||||
// guesses from the first row the geometry types involved
|
||||
// returns an empty array where there is no rows
|
||||
// return postgist types, like st_GEOTYPE
|
||||
//
|
||||
getGeometryTypes: function () {
|
||||
var row = null;
|
||||
var i = this.size();
|
||||
while (i-- && !(row && row.get('the_geom'))) {
|
||||
row = this.at(i);
|
||||
}
|
||||
if (!row) return [];
|
||||
var geom = row.get('the_geom') || row.get('the_geom_webmercator');
|
||||
var geoType = this.geometryTypeFromWKB(geom) || this.geometryTypeFromWKT(geom);
|
||||
if (geoType) {
|
||||
return ['ST_' + geoType[0].toUpperCase() + geoType.substring(1).toLowerCase()];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
});
|
||||
48
lib/assets/javascripts/dashboard/data/table/column-model.js
Normal file
48
lib/assets/javascripts/dashboard/data/table/column-model.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
idAttribute: 'name',
|
||||
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('column', method);
|
||||
var table = this.table || this.collection.table;
|
||||
if (!table) {
|
||||
console.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 (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
this.table = this.get('table');
|
||||
if (!this.table) {
|
||||
throw 'you should specify a table model'; // eslint-disable-line
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
});
|
||||
158
lib/assets/javascripts/dashboard/data/table/row-model.js
Normal file
158
lib/assets/javascripts/dashboard/data/table/row-model.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const SQL = require('internal-carto.js').SQL;
|
||||
const WKT = require('dashboard/common/wkt');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
const RESERVED_COLUMNS = 'the_geom the_geom_webmercator cartodb_id created_at updated_at'.split(' ');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
_GEOMETRY_TYPES: {
|
||||
'point': 'st_point',
|
||||
'multipoint': 'st_multipoint',
|
||||
'linestring': 'st_linestring',
|
||||
'multilinestring': 'st_multilinestring',
|
||||
'polygon': 'st_polygon',
|
||||
'multipolygon': 'st_multipolygon'
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('record', method);
|
||||
var table = this.table || this.collection.table;
|
||||
if (!table) {
|
||||
console.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 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 SQL({
|
||||
user: username,
|
||||
version: 'v2',
|
||||
api_key: api_key,
|
||||
sql_api_template: this._configModel.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 (this.parse(data.rows[0])) {
|
||||
this.set(data.rows[0]);//, {silent: silent});
|
||||
this.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 = 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 != ''); // eslint-disable-line eqeqeq
|
||||
},
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
},
|
||||
|
||||
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) {
|
||||
console.info('Not possible to parse geometry type');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getGeomType: function () {
|
||||
try {
|
||||
return this._GEOMETRY_TYPES[this.getFeatureType()];
|
||||
} catch (e) {
|
||||
console.info('Not possible to parse geometry type');
|
||||
}
|
||||
}
|
||||
|
||||
}, {
|
||||
RESERVED_COLUMNS,
|
||||
isReservedColumn: function (c) {
|
||||
return _(RESERVED_COLUMNS).indexOf(c) !== -1;
|
||||
}
|
||||
});
|
||||
102
lib/assets/javascripts/dashboard/data/table/sqlviewdata-model.js
Normal file
102
lib/assets/javascripts/dashboard/data/table/sqlviewdata-model.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const _ = require('underscore');
|
||||
const CartoTableData = require('dashboard/data/table/carto-table-data');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* contains data for a sql view
|
||||
* var s = new cdb.admin.SQLViewData({ sql : "select...." });
|
||||
* s.fetch();
|
||||
*/
|
||||
module.exports = CartoTableData.extend({
|
||||
|
||||
UNDEFINED_TYPE_COLUMN: 'undefined',
|
||||
|
||||
initialize: function (models, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this.model.prototype.idAttribute = 'cartodb_id';
|
||||
CartoTableData.prototype.initialize.call(this, models, options);
|
||||
|
||||
this.bind('error', function () {
|
||||
this.reset([]);
|
||||
});
|
||||
// this.initOptions();
|
||||
if (options && options.sql) {
|
||||
this.setSQL(options.sql);
|
||||
}
|
||||
},
|
||||
|
||||
_parseSQL: function (sql) {
|
||||
sql = sql.replace(/([^\\]){x}/g, '$10').replace(/\\{x}/g, '{x}');
|
||||
sql = sql.replace(/([^\\]){y}/g, '$10').replace(/\\{y}/g, '{y}');
|
||||
sql = sql.replace(/([^\\]){z}/g, '$10').replace(/\\{z}/g, '{z}');
|
||||
|
||||
// Substitute mapnik tokens
|
||||
// resolution at zoom level 0
|
||||
var res = '156543.03515625';
|
||||
// full webmercator extent
|
||||
var ext = 'ST_MakeEnvelope(-20037508.5,-20037508.5,20037508.5,20037508.5,3857)';
|
||||
sql = sql.replace('!bbox!', ext)
|
||||
.replace('!pixel_width!', res)
|
||||
.replace('!pixel_height!', res);
|
||||
|
||||
return sql;
|
||||
},
|
||||
|
||||
sqlSource: function () {
|
||||
return this.options.get('sql_source');
|
||||
},
|
||||
|
||||
setSQL: function (sql, opts) {
|
||||
opts = opts || {};
|
||||
// reset options whiout changing raising a new fetchs
|
||||
this.options.set({
|
||||
page: 0,
|
||||
sort_order: 'asc',
|
||||
order_by: '',
|
||||
filter_column: '',
|
||||
filter_value: '',
|
||||
sql_source: opts.sql_source || null
|
||||
}, { silent: true });
|
||||
|
||||
var silent = opts.silent;
|
||||
opts.silent = true;
|
||||
this.options.set({ sql: sql ? this._parseSQL(sql) : '' }, opts);
|
||||
if (!silent) {
|
||||
this.options.trigger('change:sql', this.options, sql);
|
||||
}
|
||||
},
|
||||
|
||||
getSQL: function () {
|
||||
return this.options.get('sql');
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return this.sqlApiUrl();
|
||||
},
|
||||
|
||||
isReadOnly: function () {
|
||||
return this.sqlSource() !== 'filters';
|
||||
},
|
||||
|
||||
quartiles: function (nslots, column, callback, error) {
|
||||
var tmpl = _.template('SELECT quartile, max(<%= column %>) as maxAmount FROM (SELECT <%= column %>, ntile(<%= slots %>) over (order by <%= column %>) as quartile FROM (<%= sql %>) as _rambo WHERE <%= column %> IS NOT NULL) x GROUP BY quartile ORDER BY quartile');
|
||||
this._sqlQuery(tmpl({
|
||||
slots: nslots,
|
||||
sql: this.options.get('sql'),
|
||||
column: column
|
||||
}),
|
||||
function (data) {
|
||||
callback(_(data.rows).pluck('maxamount'));
|
||||
},
|
||||
error);
|
||||
},
|
||||
|
||||
// returns if the query contains geo data
|
||||
isGeoreferenced: function () {
|
||||
return this.getGeometryTypes().length > 0;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
fetched: false,
|
||||
|
||||
initialize: function () {
|
||||
this.bind('sync', () => {
|
||||
this.fetched = true;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* get value for row index and columnName
|
||||
*/
|
||||
getCell: function (index, columnName) {
|
||||
var r = this.at(index);
|
||||
if (!r) {
|
||||
return null;
|
||||
}
|
||||
return r.get(columnName);
|
||||
},
|
||||
|
||||
isEmpty: function () {
|
||||
return this.length === 0;
|
||||
}
|
||||
|
||||
});
|
||||
119
lib/assets/javascripts/dashboard/data/upload-model.js
Normal file
119
lib/assets/javascripts/dashboard/data/upload-model.js
Normal file
@@ -0,0 +1,119 @@
|
||||
var _ = require('underscore');
|
||||
var UploadConfig = require('dashboard/common/upload-config');
|
||||
var UploadModel = require('builder/data/upload-model');
|
||||
var Utils = require('builder/helpers/utils.js');
|
||||
var moment = require('moment');
|
||||
|
||||
module.exports = UploadModel.extend({
|
||||
validate: function (attrs) {
|
||||
if (!attrs) return;
|
||||
|
||||
if (attrs.type === 'file') {
|
||||
// Number of files
|
||||
if (attrs.value && attrs.value.length) {
|
||||
return {
|
||||
msg: _t('data.upload-model.one-file')
|
||||
};
|
||||
}
|
||||
|
||||
// File name
|
||||
var name = attrs.value.name;
|
||||
if (!name) {
|
||||
return {
|
||||
msg: _t('data.upload-model.file-defined')
|
||||
};
|
||||
}
|
||||
|
||||
// File extension
|
||||
var ext = name.substr(name.lastIndexOf('.') + 1);
|
||||
if (ext) {
|
||||
ext = ext.toLowerCase();
|
||||
}
|
||||
if (!_.contains(UploadConfig.fileExtensions, ext)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.file-extension')
|
||||
};
|
||||
}
|
||||
// File size
|
||||
if ((this._userModel.get('remaining_byte_quota') * UploadConfig.fileTimesBigger) < attrs.value.size) {
|
||||
return {
|
||||
msg: _t('data.upload-model.file-size'),
|
||||
error_code: 8001
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'remote') {
|
||||
// Valid remote visualization id?
|
||||
if (!attrs.remote_visualization_id) {
|
||||
return {
|
||||
msg: _t('data.upload-model.visualization-id')
|
||||
};
|
||||
}
|
||||
// Remote size?
|
||||
if (attrs.size && ((this._userModel.get('remaining_byte_quota') * UploadConfig.fileTimesBigger) < attrs.size)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.remote-file-size'),
|
||||
error_code: 8001
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'url') {
|
||||
// Valid URL?
|
||||
if (!Utils.isURL(attrs.value)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.url-invalid')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'sql') {
|
||||
if (!attrs.value) {
|
||||
return {
|
||||
msg: _t('data.upload-model.query-undefined')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'duplication') {
|
||||
if (!attrs.value) {
|
||||
return {
|
||||
msg: _t('data.upload-model.dataset-copy-undefined')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'service' && attrs.service_name === 'twitter_search') {
|
||||
var service_item_id = attrs.service_item_id;
|
||||
|
||||
// Empty?
|
||||
if (!service_item_id || _.isEmpty(service_item_id)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.twitter-data')
|
||||
};
|
||||
}
|
||||
|
||||
// Categories?
|
||||
if (_.isEmpty(service_item_id.categories)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.twitter-categories-invalid')
|
||||
};
|
||||
}
|
||||
|
||||
// Dates?
|
||||
var dates = service_item_id.dates;
|
||||
if (!dates || _.isEmpty(dates)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.twitter-dates-empty')
|
||||
};
|
||||
}
|
||||
var isToDateValid = moment(dates.fromDate) <= moment(new Date());
|
||||
if (!dates.fromDate || !dates.toDate || !isToDateValid) {
|
||||
return {
|
||||
msg: _t('data.upload-model.twitter-dates-invalid')
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
53
lib/assets/javascripts/dashboard/data/url-model.js
Normal file
53
lib/assets/javascripts/dashboard/data/url-model.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Represents a URL.
|
||||
* Provides common semantics to manipulate a URL without having to resort to manipulating strings manually.
|
||||
* Rather don't subclass but you composition if you need to extend some functionality.
|
||||
*
|
||||
* Can safely be coerced into a string implicitly, e.g.:
|
||||
* const myUrl = UrlModel.byBasePath('http://foobar.com/some/path')
|
||||
* alert(myUrl); // will output 'http://foobar.com/some/path'
|
||||
*/
|
||||
|
||||
const UrlModel = Backbone.Model.extend({
|
||||
initialize: function (attrs) {
|
||||
if (!attrs.base_url) {
|
||||
throw new Error('base_url is required');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a new URL object with new basepath.
|
||||
* @param {String,*} path new sub path. Slashes are not necessary, e.g. 'my_path'
|
||||
* @return {Object} instance of cdb.common.Url
|
||||
*/
|
||||
urlToPath: function () {
|
||||
return UrlModel.byBaseUrl(this.toString.apply(this, arguments));
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {String} Path of this URL, e.g. '/some/path'
|
||||
*/
|
||||
pathname: function () {
|
||||
return this.toString().match(/^.+\/\/[^\/]+(.*)$/)[1];
|
||||
},
|
||||
|
||||
toString: function () {
|
||||
return this._joinArgumentsWithSlashes(
|
||||
this.get('base_url'),
|
||||
Array.prototype.slice.call(arguments, 0)
|
||||
);
|
||||
},
|
||||
|
||||
_joinArgumentsWithSlashes: function () {
|
||||
return _.chain(arguments).flatten().compact().value().join('/');
|
||||
}
|
||||
}, {
|
||||
byBaseUrl: function (url) {
|
||||
return new UrlModel({ base_url: url });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UrlModel;
|
||||
@@ -0,0 +1,23 @@
|
||||
const Backbone = require('backbone');
|
||||
const GroupModel = require('dashboard/data/group-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (attrs, { collection }) {
|
||||
return new GroupModel(attrs, {
|
||||
collection,
|
||||
configModel: collection._configModel
|
||||
});
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
this.organization = opts.organization;
|
||||
}
|
||||
|
||||
});
|
||||
269
lib/assets/javascripts/dashboard/data/user-model.js
Normal file
269
lib/assets/javascripts/dashboard/data/user-model.js
Normal file
@@ -0,0 +1,269 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const UserUrlModel = require('dashboard/data/user-url-model');
|
||||
|
||||
const UserModel = Backbone.Model.extend({
|
||||
urlRoot: '/api/v1/users',
|
||||
|
||||
defaults: {
|
||||
avatar_url: 'http://cartodb.s3.amazonaws.com/static/public_dashboard_default_avatar.png',
|
||||
username: ''
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
attrs = attrs || {};
|
||||
opts = opts || {};
|
||||
this.tables = [];
|
||||
// Removing avatar_url attribute if it comes as null
|
||||
// Due to a Backbone Model constructor uses _.extends
|
||||
// instead of _.defaults
|
||||
if (this.get('avatar_url') === null) {
|
||||
this.set('avatar_url', this.defaults.avatar_url);
|
||||
}
|
||||
|
||||
this.email = (typeof attrs.email !== 'undefined') ? attrs.email : '';
|
||||
|
||||
if (opts.groups) {
|
||||
this.setGroups(opts.groups);
|
||||
}
|
||||
|
||||
if (opts.organization) {
|
||||
this.setOrganization(opts.organization);
|
||||
}
|
||||
},
|
||||
|
||||
setGroups: function (groups) {
|
||||
this.groups = groups;
|
||||
},
|
||||
|
||||
setOrganization: function (organization) {
|
||||
this.organization = organization;
|
||||
|
||||
if (this.groups) {
|
||||
this.groups.organization = organization;
|
||||
}
|
||||
},
|
||||
|
||||
isInsideOrg: function () {
|
||||
if (this.organization) {
|
||||
return this.organization.id !== false || this.isOrgOwner();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isAuthUsernamePasswordEnabled: function () {
|
||||
if (this.organization) {
|
||||
return this.organization.get('auth_username_password_enabled');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isOrgOwner: function () {
|
||||
if (this.organization) {
|
||||
return this.organization.owner.get('id') === this.get('id');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isOrgAdmin: function () {
|
||||
if (this.organization) {
|
||||
return this.organization.isOrgAdmin(this);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isViewer: function () {
|
||||
return this.get('viewer') === true;
|
||||
},
|
||||
|
||||
isBuilder: function () {
|
||||
return !this.isViewer();
|
||||
},
|
||||
|
||||
nameOrUsername: function () {
|
||||
return this.fullName() || this.get('username');
|
||||
},
|
||||
|
||||
fullName: function () {
|
||||
var name = this.get('name') || '';
|
||||
var lastName = this.get('last_name') || '';
|
||||
if (name || lastName) {
|
||||
return name + (name && lastName ? ' ' : '') + lastName;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
renderData: function (currentUser) {
|
||||
var name = this.get('username');
|
||||
|
||||
if (currentUser && currentUser.id === this.id) {
|
||||
name = _t('You');
|
||||
}
|
||||
|
||||
return {
|
||||
username: name,
|
||||
avatar_url: this.get('avatar_url')
|
||||
};
|
||||
},
|
||||
|
||||
hasCreateDatasetsFeature: function () {
|
||||
return this.isBuilder();
|
||||
},
|
||||
|
||||
canCreateDatasets: function () {
|
||||
if (!this.get('remaining_byte_quota') || this.get('remaining_byte_quota') <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.hasCreateDatasetsFeature();
|
||||
},
|
||||
|
||||
hasCreateMapsFeature: function () {
|
||||
return this.isBuilder();
|
||||
},
|
||||
|
||||
canAddLayerTo: function (map) {
|
||||
if (!map || !map.layers || !map.layers.getDataLayers) {
|
||||
throw new Error('Map model is not defined or wrong');
|
||||
}
|
||||
var dataLayers = map.layers.getDataLayers();
|
||||
return dataLayers.length < this.getMaxLayers();
|
||||
},
|
||||
|
||||
getMaxLayers: function () {
|
||||
return (this.get('limits') && this.get('limits').max_layers) || 5;
|
||||
},
|
||||
|
||||
getMaxConcurrentImports: function () {
|
||||
return (this.get('limits') && this.get('limits').concurrent_imports) || 1;
|
||||
},
|
||||
|
||||
featureEnabled: function (name) {
|
||||
var featureFlags = this.get('feature_flags');
|
||||
|
||||
if (!featureFlags || featureFlags.length === 0 || !name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _.contains(featureFlags, name);
|
||||
},
|
||||
|
||||
isCloseToLimits: function () {
|
||||
var quota = this.get('quota_in_bytes');
|
||||
var remainingQuota = this.get('remaining_byte_quota');
|
||||
|
||||
return ((remainingQuota * 100) / quota) < 20;
|
||||
},
|
||||
|
||||
isEnterprise: function () {
|
||||
return this.get('account_type').toLowerCase().indexOf('enterprise') != -1; // eslint-disable-line
|
||||
},
|
||||
|
||||
getOrganizationName: function () {
|
||||
return this.isInsideOrg() ? this.organization.get('name') : '';
|
||||
},
|
||||
|
||||
getOrgName: function () {
|
||||
return this.isInsideOrg() ? this.organization.get('name') : '';
|
||||
},
|
||||
|
||||
getMaxLayersPerMap: function () {
|
||||
return this.get('max_layers') || 4;
|
||||
},
|
||||
|
||||
canStartTrial: function () {
|
||||
return !this.isInsideOrg() && this.get('account_type') === 'FREE' && this.get('table_count') > 0;
|
||||
},
|
||||
|
||||
canCreatePrivateDatasets: function () {
|
||||
var actions = this.get('actions');
|
||||
return actions && actions.private_tables;
|
||||
},
|
||||
|
||||
canCreateTwitterDataset: function () {
|
||||
var twitter = this.get('twitter');
|
||||
return !(twitter.quota - twitter.monthly_use) <= 0 && twitter.hard_limit;
|
||||
},
|
||||
|
||||
canSelectPremiumOptions: function (visModel) {
|
||||
return this.get('actions')[ visModel.isVisualization() ? 'private_maps' : 'private_tables' ];
|
||||
},
|
||||
|
||||
hasOwnTwitterCredentials: function () {
|
||||
var twitter = this.get('twitter');
|
||||
return (twitter && twitter.customized_config) || false;
|
||||
},
|
||||
|
||||
equals: function (otherUser) {
|
||||
if (otherUser.get) {
|
||||
return this.get('id') === otherUser.get('id');
|
||||
}
|
||||
},
|
||||
|
||||
viewUrl: function () {
|
||||
return new UserUrlModel({
|
||||
base_url: this.get('base_url'),
|
||||
is_org_admin: this.isOrgAdmin()
|
||||
});
|
||||
},
|
||||
|
||||
upgradeContactEmail: function () {
|
||||
if (this.isInsideOrg()) {
|
||||
if (this.isOrgOwner()) {
|
||||
return 'enterprise-support@carto.com';
|
||||
} else {
|
||||
return this.organization.owner.get('email');
|
||||
}
|
||||
} else {
|
||||
return 'support@carto.com';
|
||||
}
|
||||
},
|
||||
|
||||
needsPasswordConfirmation: function () {
|
||||
return this.get('needs_password_confirmation');
|
||||
},
|
||||
|
||||
usedQuotaPercentage: function () {
|
||||
return (this.get('db_size_in_bytes') * 100) / this.organization.get('available_quota_for_user');
|
||||
},
|
||||
|
||||
assignedQuotaInRoundedMb: function () {
|
||||
return Math.floor(this.get('quota_in_bytes') / 1024 / 1024).toFixed(0);
|
||||
},
|
||||
|
||||
assignedQuotaPercentage: function () {
|
||||
return (this.get('quota_in_bytes') * 100) / this.organization.get('available_quota_for_user');
|
||||
},
|
||||
|
||||
getGoogleApiKey: function () {
|
||||
return this.get('google_maps_private_key');
|
||||
},
|
||||
|
||||
hasGoogleMaps: function () {
|
||||
return !!this.getGoogleApiKey();
|
||||
},
|
||||
|
||||
showGoogleApiKeys: function () {
|
||||
return this.hasGoogleMaps() && (!this.isInsideOrg() || this.isOrgOwner());
|
||||
},
|
||||
|
||||
getSchema: function () {
|
||||
return this.isInsideOrg() ? this.get('username') : 'public';
|
||||
},
|
||||
|
||||
getAuthToken: function () {
|
||||
return btoa(`${this.get('username')}:${this.get('api_key')}`);
|
||||
},
|
||||
|
||||
getModelType: () => 'user',
|
||||
|
||||
isActionEnabled: function (action) {
|
||||
return this.get('actions') && this.get('actions')[action];
|
||||
},
|
||||
|
||||
hasAccountType: function (accountType) {
|
||||
return this.get('account_type') === accountType;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UserModel;
|
||||
118
lib/assets/javascripts/dashboard/data/user-tables-model.js
Normal file
118
lib/assets/javascripts/dashboard/data/user-tables-model.js
Normal file
@@ -0,0 +1,118 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel'
|
||||
];
|
||||
|
||||
const PUBLIC_PRIVACIES = ['PUBLIC', 'LINK'];
|
||||
|
||||
const DEFAULT_ATTRS = {
|
||||
select: false,
|
||||
update: false,
|
||||
insert: false,
|
||||
delete: false
|
||||
};
|
||||
|
||||
const STATUS = {
|
||||
fetching: 'fetching',
|
||||
fetched: 'fetched',
|
||||
errored: 'errored'
|
||||
};
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
initialize: function (attrs, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.stateModel = new Backbone.Model({
|
||||
status: STATUS.fetching
|
||||
});
|
||||
|
||||
this.paramsModel = new Backbone.Model({
|
||||
tag_name: '',
|
||||
q: '',
|
||||
page: 1,
|
||||
type: '',
|
||||
exclude_shared: true,
|
||||
tags: '',
|
||||
shared: 'no',
|
||||
only_liked: false,
|
||||
order: 'updated_at',
|
||||
types: 'table',
|
||||
deepInsights: false
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.paramsModel, 'change:q change:privacy', () => this.fetch());
|
||||
|
||||
this.on('sync', () => this.stateModel.set({ status: STATUS.fetched }));
|
||||
this.on('error', () => this.stateModel.set({ status: STATUS.errored }));
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return `${this._userModel.get('base_url')}/api/v1/viz?${this.generateParams()}`;
|
||||
},
|
||||
|
||||
generateParams: function () {
|
||||
return $.param(this.paramsModel.attributes);
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
this.stateModel.set({ status: STATUS.fetching });
|
||||
return Backbone.Model.prototype.fetch.apply(this, arguments);
|
||||
},
|
||||
|
||||
parse: function (response) {
|
||||
this.attributes = {};
|
||||
|
||||
const tables = response.visualizations;
|
||||
|
||||
return tables.reduce((total, table) => ({
|
||||
...total,
|
||||
[table.name]: {
|
||||
permissions: {
|
||||
...DEFAULT_ATTRS,
|
||||
select: this.paramsModel.get('privacy') ? _.contains(PUBLIC_PRIVACIES, table.privacy) : false
|
||||
}
|
||||
}
|
||||
}), {});
|
||||
},
|
||||
|
||||
setQuery: function (q) {
|
||||
this.paramsModel.set({ q });
|
||||
},
|
||||
|
||||
clearParams: function () {
|
||||
this.paramsModel.set({ q: '' });
|
||||
this.paramsModel.unset('privacy');
|
||||
},
|
||||
|
||||
getStateModel: function () {
|
||||
return this.stateModel;
|
||||
},
|
||||
|
||||
isFetched: function () {
|
||||
return this.stateModel.get('status') === STATUS.fetched;
|
||||
},
|
||||
|
||||
hasQuery: function () {
|
||||
return !!this.paramsModel.get('q');
|
||||
},
|
||||
|
||||
isEmpty: function () {
|
||||
return _.isEmpty(this.attributes);
|
||||
},
|
||||
|
||||
fetchPublicDatasets: function () {
|
||||
this.paramsModel.set({
|
||||
privacy: ['public', 'link'],
|
||||
per_page: 200 // TODO: how do we show everything?
|
||||
});
|
||||
}
|
||||
});
|
||||
61
lib/assets/javascripts/dashboard/data/user-url-model.js
Normal file
61
lib/assets/javascripts/dashboard/data/user-url-model.js
Normal file
@@ -0,0 +1,61 @@
|
||||
var _ = require('underscore');
|
||||
var UrlModel = require('dashboard/data/url-model');
|
||||
var DashboardUrlModel = require('dashboard/data/dashboard-url-model');
|
||||
var OrganizationUrlModel = require('dashboard/data/organization-url-model');
|
||||
|
||||
/**
|
||||
* URLs associated with a particular user.
|
||||
*/
|
||||
var UserUrlModel = UrlModel.extend({
|
||||
initialize: function (attrs) {
|
||||
UrlModel.prototype.initialize.apply(this, arguments);
|
||||
|
||||
if (_.isUndefined(attrs.is_org_admin)) {
|
||||
throw new Error('is_org_admin is required');
|
||||
}
|
||||
},
|
||||
|
||||
organization: function () {
|
||||
if (this.get('is_org_admin')) {
|
||||
return new OrganizationUrlModel({
|
||||
base_url: this.urlToPath('organization')
|
||||
});
|
||||
} else {
|
||||
return this.urlToPath('account');
|
||||
}
|
||||
},
|
||||
|
||||
accountProfile: function () {
|
||||
return this.urlToPath('profile');
|
||||
},
|
||||
|
||||
accountSettings: function () {
|
||||
return this.urlToPath('account');
|
||||
},
|
||||
|
||||
publicProfile: function () {
|
||||
return this.urlToPath('me');
|
||||
},
|
||||
|
||||
apiKeys: function () {
|
||||
return this.urlToPath('your_apps');
|
||||
},
|
||||
|
||||
logout: function () {
|
||||
return this.urlToPath('logout');
|
||||
},
|
||||
|
||||
dashboard: function () {
|
||||
return new DashboardUrlModel({
|
||||
base_url: this.urlToPath('dashboard')
|
||||
});
|
||||
},
|
||||
|
||||
connectedApps: function () {
|
||||
return new DashboardUrlModel({
|
||||
base_url: this.urlToPath('dashboard/connected_apps')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UserUrlModel;
|
||||
253
lib/assets/javascripts/dashboard/data/visualization-model.js
Normal file
253
lib/assets/javascripts/dashboard/data/visualization-model.js
Normal file
@@ -0,0 +1,253 @@
|
||||
const Backbone = require('backbone');
|
||||
const _ = require('underscore');
|
||||
const LikeModel = require('dashboard/data/like-model');
|
||||
const UserModel = require('dashboard/data/user-model');
|
||||
const PermissionModel = require('dashboard/data/permission-model');
|
||||
const MapModel = require('dashboard/data/map-model');
|
||||
const VisualizationOrderModel = require('dashboard/data/visualization-order-model');
|
||||
const SlideTransition = require('dashboard/data/slide-transition-model');
|
||||
const MapUrlModel = require('dashboard/data/map-url-model');
|
||||
const DatasetUrlModel = require('dashboard/data/dataset-url-model');
|
||||
const CartoTableMetadata = require('dashboard/views/public-dataset/carto-table-metadata');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
const PRIVACY_OPTIONS = {
|
||||
public: 'PUBLIC',
|
||||
link: 'LINK',
|
||||
private: 'PRIVATE',
|
||||
password: 'PASSWORD'
|
||||
};
|
||||
|
||||
const VisualizationModel = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
bindMap: true
|
||||
},
|
||||
|
||||
INHERIT_TABLE_ATTRIBUTES: [
|
||||
'name', 'description', 'privacy'
|
||||
],
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this.map = new MapModel({ configModel: this._configModel });
|
||||
this.permission = new PermissionModel(this.get('permission'), { configModel: this._configModel });
|
||||
this.order = new VisualizationOrderModel({ visualization: this });
|
||||
this.transition = new SlideTransition(this.get('transition_options'), { parse: true });
|
||||
|
||||
this.like = LikeModel.newByVisData({
|
||||
vis_id: this.id,
|
||||
liked: this.get('liked'),
|
||||
likes: this.get('likes'),
|
||||
config: this._configModel
|
||||
});
|
||||
|
||||
if (this.get('bindMap')) this._bindMap();
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.permission.acl.bind('reset', function () {
|
||||
// Sync the local permission object w/ the raw data, so vis.save don't accidentally overwrites permissions changes
|
||||
this.set('permission', this.permission.attributes, { silent: true });
|
||||
this.trigger('change:permission', this);
|
||||
}, this);
|
||||
|
||||
// Keep permission model in sync, e.g. on vis.save
|
||||
this.bind('change:permission', function () {
|
||||
this.permission.set(this.get('permission'));
|
||||
}, this);
|
||||
},
|
||||
|
||||
_bindMap: function () {
|
||||
this.on('change:map_id', this._fetchMap, this);
|
||||
|
||||
this.map.bind('change:id', function () {
|
||||
this.set('map_id', this.map.id);
|
||||
}, this);
|
||||
|
||||
this.map.set('id', this.get('map_id'));
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('visualization', method);
|
||||
var base = '/api/' + version + '/viz';
|
||||
if (this.isNew()) {
|
||||
return base;
|
||||
}
|
||||
return base + '/' + this.id;
|
||||
},
|
||||
|
||||
parse: function (data) {
|
||||
if (this.transition && data.transition_options) {
|
||||
this.transition.set(this.transition.parse(data.transition_options));
|
||||
}
|
||||
|
||||
if (this.like) {
|
||||
this.like.set({
|
||||
vis_id: this.id,
|
||||
likes: this.get('likes'),
|
||||
liked: this.get('liked')
|
||||
});
|
||||
}
|
||||
|
||||
if (this.owner) {
|
||||
this.owner = new UserModel(this.owner);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
var attr = _.clone(this.attributes);
|
||||
|
||||
delete attr.bindMap;
|
||||
delete attr.stats;
|
||||
delete attr.related_tables;
|
||||
delete attr.children;
|
||||
|
||||
attr.transition_options = this.transition.toJSON();
|
||||
|
||||
return attr;
|
||||
},
|
||||
|
||||
getLikesModel: function () {
|
||||
return this.like;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a copy of the visualization model
|
||||
*/
|
||||
copy: function (attrs, options) {
|
||||
attrs = attrs || {};
|
||||
options = options || {};
|
||||
var vis = new VisualizationModel(
|
||||
_.extend({
|
||||
source_visualization_id: this.id
|
||||
},
|
||||
attrs
|
||||
),
|
||||
{ configModel: this._configModel }
|
||||
);
|
||||
vis.save(null, options);
|
||||
return vis;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch map information
|
||||
*/
|
||||
_fetchMap: function () {
|
||||
this.map
|
||||
.set('id', this.get('map_id'))
|
||||
.fetch();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the URL for current instance.
|
||||
* @param {Object} currentUser (Optional) Get the URL from the perspective of the current user, necessary to
|
||||
* correctly setup URLs to tables.
|
||||
* @return {Object} instance of cdb.common.Url
|
||||
*/
|
||||
viewUrl: function (currentUser) {
|
||||
const owner = this.permission.owner;
|
||||
let userUrl = this.permission.owner.viewUrl();
|
||||
|
||||
// the undefined check is required for backward compability, in some cases (e.g. dependant visualizations) the type
|
||||
// is not available on the attrs, if so assume the old behavior (e.g. it's a visualization/derived/map).
|
||||
if (this.isVisualization() || _.isUndefined(this.get('type'))) {
|
||||
let id = this.get('id');
|
||||
|
||||
if (currentUser && currentUser.id !== owner.id && this.permission.hasAccess(currentUser)) {
|
||||
userUrl = currentUser.viewUrl();
|
||||
id = owner.get('username') + '.' + id;
|
||||
}
|
||||
|
||||
return new MapUrlModel({
|
||||
base_url: userUrl.urlToPath('viz', id)
|
||||
});
|
||||
} else {
|
||||
if (currentUser && this.permission.hasAccess(currentUser)) {
|
||||
userUrl = currentUser.viewUrl();
|
||||
}
|
||||
return new DatasetUrlModel({
|
||||
base_url: userUrl.urlToPath('tables', this.tableMetadata().getUnquotedName())
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// return: Array of entities (user or organizations) this vis is shared with
|
||||
sharedWithEntities: function () {
|
||||
return _.map((this.permission.acl.toArray() || []), function (aclItem) {
|
||||
return aclItem.get('entity');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Is this model a true visualization?
|
||||
*/
|
||||
isVisualization: function () {
|
||||
return this.get('type') === 'derived' || this.get('type') === 'slide';
|
||||
},
|
||||
|
||||
/**
|
||||
* Get table metadata related to this vis.
|
||||
* Note that you might need to do a {metadata.fetch()} to get full data.
|
||||
*
|
||||
* @returns {CartoTableMetadata} if this vis represents a table
|
||||
* TODO: when and when isn't it required to do a fetch really?
|
||||
*/
|
||||
tableMetadata: function () {
|
||||
if (!this._metadata) {
|
||||
this._metadata = new CartoTableMetadata(this.get('table'), { configModel: this._configModel });
|
||||
}
|
||||
return this._metadata;
|
||||
},
|
||||
|
||||
getTableModel: function () {
|
||||
if (!this._metadata) {
|
||||
this._metadata = new CartoTableMetadata(this.get('table'), { configModel: this._configModel });
|
||||
}
|
||||
return this._metadata;
|
||||
},
|
||||
|
||||
privacyOptions: function () {
|
||||
const privacyOptions = _.values(PRIVACY_OPTIONS);
|
||||
|
||||
if (this.isVisualization()) {
|
||||
return privacyOptions;
|
||||
} else {
|
||||
return _.filter(privacyOptions, option => option !== 'PASSWORD');
|
||||
}
|
||||
},
|
||||
|
||||
isRaster: function () {
|
||||
return this.get('kind') === 'raster';
|
||||
},
|
||||
|
||||
getPermissionModel: function () {
|
||||
return this.permission;
|
||||
},
|
||||
|
||||
getSynchronizationModel: function () {
|
||||
return this._synchronizationModel;
|
||||
},
|
||||
|
||||
mapcapsURL: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/v3/viz/' + this.id + '/mapcaps';
|
||||
}
|
||||
}, {
|
||||
isPubliclyAvailable: function (privacyStatus) {
|
||||
return privacyStatus === PRIVACY_OPTIONS.password ||
|
||||
privacyStatus === PRIVACY_OPTIONS.link ||
|
||||
privacyStatus === PRIVACY_OPTIONS.public;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = VisualizationModel;
|
||||
@@ -0,0 +1,14 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
url: function (method) {
|
||||
return this.visualization.url(method) + '/next_id';
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.visualization = this.get('visualization');
|
||||
this.set('id', this.visualization.id);
|
||||
this.unset('visualization');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
const Backbone = require('backbone');
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const VisualizationModel = require('dashboard/data/visualization-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Visualizations endpoint available for a given user.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* const visualizations = new VisualizationsCollection();
|
||||
* visualizations.fetch();
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
_PREVIEW_TABLES_PER_PAGE: 10,
|
||||
_TABLES_PER_PAGE: 20,
|
||||
_PREVIEW_ITEMS_PER_PAGE: 3,
|
||||
_ITEMS_PER_PAGE: 9,
|
||||
|
||||
sync: require('dashboard/data/backbone/sync-abort'),
|
||||
|
||||
initialize: function (models, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
var default_options = new Backbone.Model({
|
||||
tag_name: '',
|
||||
q: '',
|
||||
page: 1,
|
||||
type: 'derived',
|
||||
exclude_shared: false,
|
||||
per_page: this._ITEMS_PER_PAGE
|
||||
});
|
||||
|
||||
this.options = _.extend(default_options, this.options);
|
||||
|
||||
this.total_entries = 0;
|
||||
|
||||
this.bind('reset', this._checkPage, this);
|
||||
},
|
||||
|
||||
model: function (attrs, opts) {
|
||||
const options = { ...opts, configModel: opts.collection._configModel };
|
||||
|
||||
return new VisualizationModel(attrs, options);
|
||||
},
|
||||
|
||||
getTotalPages: function () {
|
||||
return Math.ceil(this.total_entries / this.options.get('per_page'));
|
||||
},
|
||||
|
||||
_checkPage: function () {
|
||||
const total = this.getTotalPages();
|
||||
|
||||
if (this.options.get('page') > total) {
|
||||
this.options.set({ page: total + 1 });
|
||||
} else if (this.options.get('page') < 1) {
|
||||
this.options.set({ page: 1 });
|
||||
}
|
||||
},
|
||||
|
||||
_createUrlOptions: function () {
|
||||
const urlParams = _(this.options.attributes).map((v, k) => `${k}=${encodeURIComponent(v)}`);
|
||||
|
||||
return _.compact(urlParams).join('&');
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
const version = this._configModel.urlVersion('visualizations', method);
|
||||
|
||||
return `/api/${version}/viz?${this._createUrlOptions()}`;
|
||||
},
|
||||
|
||||
remove: function (options) {
|
||||
this.total_entries--;
|
||||
|
||||
Backbone.Collection.prototype.remove.apply(this, arguments);
|
||||
},
|
||||
|
||||
// add bindMap: false for all the visulizations
|
||||
// vis model does not need map information in dashboard
|
||||
parse: function (response) {
|
||||
this.total_entries = response.total_entries;
|
||||
this.slides && this.slides.reset(response.children);
|
||||
this.total_shared = response.total_shared;
|
||||
this.total_likes = response.total_likes;
|
||||
this.total_user_entries = response.total_user_entries;
|
||||
|
||||
return response.visualizations.map(vis => ({ ...vis, bindMap: false }));
|
||||
},
|
||||
|
||||
create: function (model) {
|
||||
const deferred = $.Deferred();
|
||||
|
||||
Backbone.Collection.prototype.create.call(this,
|
||||
model,
|
||||
{
|
||||
wait: true,
|
||||
success: () => deferred.resolve(),
|
||||
error: () => deferred.reject()
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise();
|
||||
},
|
||||
|
||||
fetch: function (opts) {
|
||||
var deferred = $.Deferred();
|
||||
var self = this;
|
||||
|
||||
this.trigger('loading', this);
|
||||
|
||||
$.when(Backbone.Collection.prototype.fetch.call(this, opts))
|
||||
.done(function (res) {
|
||||
self.trigger('loaded');
|
||||
deferred.resolve();
|
||||
})
|
||||
.fail(function (res) {
|
||||
self.trigger('loadFailed');
|
||||
deferred.reject(res);
|
||||
});
|
||||
|
||||
return deferred.promise();
|
||||
},
|
||||
|
||||
getTotalStat: function (attribute) {
|
||||
return this[attribute] || 0;
|
||||
},
|
||||
|
||||
getDefaultParam: function (param) {
|
||||
return this.options.get(param);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user