Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions

View 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`;
}
});

View File

@@ -0,0 +1,5 @@
module.exports = {
MASTER: 'master',
DEFAULT: 'default',
REGULAR: 'regular'
};

View 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;
}
});

View 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);
}
});

View File

@@ -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;
}
});

View 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;
};

View 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;
};
})();

View File

@@ -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);
}
});

View File

@@ -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;
}
});

View File

@@ -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;

View File

@@ -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');
}
});

View File

@@ -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();
});
}
});

View File

@@ -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;

View 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');
}
});

View File

@@ -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;

View 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;

View File

@@ -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;

View 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;

View 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}`;
}
});

View 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);
}
});

View File

@@ -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;
}
});

View 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'
});

View File

@@ -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;
}
});

View 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);
}
}
});

View 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();
});
}
});

View 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()
};
}
});

View 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;

View 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;

View File

@@ -0,0 +1,10 @@
const Backbone = require('backbone');
const MapLayer = Backbone.Model.extend({
defaults: {
visible: true,
type: 'Tiled'
}
});
module.exports = MapLayer;

View 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;

View 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;

View File

@@ -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;
}
});

View File

@@ -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`;
}
});

View 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'
});

View File

@@ -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;

View File

@@ -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;
}
});

View 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
});
}
});

View 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;

View File

@@ -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`;
}
});

View 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;
}
});

View File

@@ -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`;
}
});

View File

@@ -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;

View File

@@ -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
}
});

View 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 [];
}
});

View 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;
}
});

View 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;
}
});

View 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;
}
});

View File

@@ -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;
}
});

View 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')
};
}
}
}
});

View 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;

View File

@@ -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;
}
});

View 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;

View 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?
});
}
});

View 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;

View 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;

View File

@@ -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');
}
});

View File

@@ -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);
}
});