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,34 @@
var Backbone = require('backbone');
var _ = require('underscore');
var PermissionModel = require('./permission-model');
var OWN_ATTR_NAMES = ['id', 'username', 'avatar_url', 'name'];
module.exports = Backbone.Model.extend({
defaults: {
access: 'r'
},
isOwn: function (model) {
return model.id === this.get('entity').id;
},
validate: function (attrs, options) {
if (attrs.access !== PermissionModel.READ_ONLY && attrs.access !== PermissionModel.READ_WRITE) {
return "access can't take 'r' or 'rw' values";
}
},
toJSON: function () {
var entity = _.pick(this.get('entity').toJSON(), OWN_ATTR_NAMES);
// 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')
};
}
});

View File

@@ -0,0 +1,527 @@
var _ = require('underscore');
var camshaftReference = require('./camshaft-reference');
var AreaOfInfluenceFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/area-of-influence-form-model');
var AggregateIntersectionFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/aggregate-intersection-form-model');
var FilterIntersectionFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/filter-intersection-form-model');
var FallbackFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/fallback-form-model');
var UnknownTypeFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/unknown-type-form-model');
var MergeAnalysisOptionModel = require('builder/components/modals/add-analysis/analysis-option-models/merge-analysis-option-model');
var DeprecatedSQLFunctionOptionModel = require('builder/components/modals/add-analysis/analysis-option-models/deprecated-sql-function-option-model');
var DataServicesApiCheck = require('builder/editor/layers/layer-content-views/analyses/analyses-quota/analyses-quota-info');
var DAO_TYPES = ['age-and-gender', 'boundaries', 'education', 'employment', 'families', 'housing', 'income', 'language', 'migration', 'nationality', 'population-segments', 'race-and-ethnicity', 'religion', 'transportation'];
var MAP = {
'aggregate-intersection': {
title: _t('analyses.aggregate-intersection.title'),
short_title: _t('analyses.aggregate-intersection.short-title'),
FormModel: AggregateIntersectionFormModel,
modalTitle: _t('components.modals.add-analysis.option-types.aggregate-intersection.title'),
modalDesc: _t('components.modals.add-analysis.option-types.aggregate-intersection.desc'),
modalCategory: 'data_transformation',
modalLink: 'https://carto.com/learn/guides/analysis/intersect-and-aggregate/',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/aggregate-intersection.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/intersect.tpl')
},
'bounding-box': {
title: _t('analyses.bounding-box.title'),
short_title: _t('analyses.bounding-box.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/group-points-form-model'),
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/group-points.tpl'),
genericType: 'group-points'
},
'bounding-circle': {
title: _t('analyses.bounding-circle.title'),
short_title: _t('analyses.bounding-circle.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/group-points-form-model'),
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/group-points.tpl'),
genericType: 'group-points'
},
'buffer': {
title: _t('analyses.area-of-influence.title'),
short_title: _t('analyses.area-of-influence.short-title'),
FormModel: AreaOfInfluenceFormModel,
modalTitle: _t('components.modals.add-analysis.option-types.area-of-influence.title'),
modalDesc: _t('components.modals.add-analysis.option-types.area-of-influence.desc'),
modalCategory: 'data_transformation',
modalLink: 'https://carto.com/learn/guides/analysis/create-travel-distance-buffers/',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/area-of-influence.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/aoi.tpl'),
genericType: 'area-of-influence'
},
'centroid': {
title: _t('analyses.centroid.title'),
short_title: _t('analyses.centroid.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/centroid-form-model'),
modalTitle: _t('components.modals.add-analysis.option-types.centroid.title'),
modalDesc: _t('components.modals.add-analysis.option-types.centroid.desc'),
modalCategory: 'data_transformation',
modalLink: 'https://carto.com/learn/guides/analysis/create-centroids-of-geometries/',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/centroid.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/centroid.tpl'),
genericType: 'centroid'
},
'convex-hull': {
title: _t('analyses.convex-hull.title'),
short_title: _t('analyses.convex-hull.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/group-points-form-model'),
modalTitle: _t('components.modals.add-analysis.option-types.group-points.title'),
modalDesc: _t('components.modals.add-analysis.option-types.group-points.desc'),
modalCategory: 'data_transformation',
modalLink: 'https://carto.com/learn/guides/analysis/create-polygons-from-points/',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses//group-points.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/group-points.tpl'),
animationParams: {
method: _t('analyses-onboarding.placeholders.method')
},
genericType: 'group-points'
},
'concave-hull': {
title: _t('analyses.concave-hull.title'),
short_title: _t('analyses.concave-hull.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/group-points-form-model'),
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/group-points.tpl'),
genericType: 'group-points'
},
'data-observatory-measure': {
title: _t('analyses.data-observatory-measure.title'),
short_title: _t('analyses.data-observatory-measure.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/data-observatory-measure-model'),
modalTitle: _t('components.modals.add-analysis.option-types.data-observatory-measure.title'),
modalDesc: _t('components.modals.add-analysis.option-types.data-observatory-measure.desc'),
modalCategory: 'create_clean',
modalLink: 'https://carto.com/learn/guides/analysis/enrich-from-data-observatory',
checkIfEnabled: function (options) {
return checkIfDataObservatoryIsEnabled(options);
},
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/data-observatory-measure.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/data-observatory-measure.tpl'),
genericType: 'data-observatory-measure'
},
'data-observatory-multiple-measures': {
title: _t('analyses.data-observatory-measure.title'),
short_title: _t('analyses.data-observatory-measure.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/data-observatory-multiple-measure-model'),
modalTitle: _t('components.modals.add-analysis.option-types.data-observatory-measure.title'),
modalDesc: _t('components.modals.add-analysis.option-types.data-observatory-measure.desc'),
modalCategory: 'create_clean',
modalLink: 'https://carto.com/learn/guides/analysis/enrich-from-data-observatory',
checkIfEnabled: function (options) {
var dataservicesApiHealth = DataServicesApiCheck.get(options.configModel);
var doApiData = dataservicesApiHealth.getService('observatory');
var assignedQuota = doApiData ? doApiData.get('monthly_quota') : 0;
var softLimit = doApiData ? doApiData.get('soft_limit') : false;
var hideDataObservatory = assignedQuota === 0 && !softLimit;
return checkIfDataObservatoryIsEnabled(options) && !hideDataObservatory;
},
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/data-observatory-multiple-measure.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/data-observatory-measure.tpl'),
genericType: 'data-observatory-multiple-measures'
},
'filter-by-node-column': {
title: _t('analyses.filter-by-node-column.title'),
short_title: _t('analyses.filter-by-node-column.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/filter-by-node-column'),
modalTitle: _t('components.modals.add-analysis.option-types.filter-by-node-column.title'),
modalDesc: _t('components.modals.add-analysis.option-types.filter-by-node-column.desc'),
modalCategory: 'create_clean',
modalLink: 'https://carto.com/learn/guides/analysis/link-second-layer/',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/filter-by-node-column.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/filter-by-layer.tpl'),
animationParams: {
name_of_the_layer: _t('analyses-onboarding.placeholders.layer-name')
}
},
'filter-category': {
title: _t('analyses.filter.title'),
short_title: _t('analyses.filter.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/filter-form-model'),
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/filter.tpl'),
genericType: 'filter'
},
'filter-range': {
title: _t('analyses.filter.title'),
short_title: _t('analyses.filter.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/filter-form-model'),
modalTitle: _t('components.modals.add-analysis.option-types.filter.title'),
modalDesc: _t('components.modals.add-analysis.option-types.filter.desc'),
modalCategory: 'data_transformation',
modalLink: 'https://carto.com/learn/guides/analysis/filter-by-column-value',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/filter.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/filter-by-column-value.tpl'),
genericType: 'filter'
},
'georeference-city': {
title: _t('analyses.georeference.title'),
short_title: _t('analyses.georeference.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
checkIfEnabled: function (options) {
return checkIfInternalGeocodingIsEnabled(options);
},
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
genericType: 'georeference'
},
'georeference-ip-address': {
title: _t('analyses.georeference.title'),
short_title: _t('analyses.georeference.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
checkIfEnabled: function (options) {
return checkIfInternalGeocodingIsEnabled(options);
},
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
genericType: 'georeference'
},
'georeference-country': {
title: _t('analyses.georeference.title'),
short_title: _t('analyses.georeference.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
checkIfEnabled: function (options) {
return checkIfInternalGeocodingIsEnabled(options);
},
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
genericType: 'georeference'
},
'georeference-long-lat': {
title: _t('analyses.georeference.title'),
short_title: _t('analyses.georeference.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
genericType: 'georeference'
},
'georeference-placeholder': {
dummy: true,
title: _t('analyses.georeference.title'),
short_title: _t('analyses.georeference.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
modalTitle: _t('components.modals.add-analysis.option-types.georeference.title'),
modalDesc: _t('components.modals.add-analysis.option-types.georeference.desc'),
modalCategory: 'create_clean',
modalLink: 'https://carto.com/learn/guides/analysis/georeference',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/georeference.tpl'),
genericType: 'georeference'
},
'georeference-postal-code': {
title: _t('analyses.georeference.title'),
short_title: _t('analyses.georeference.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
checkIfEnabled: function (options) {
return checkIfInternalGeocodingIsEnabled(options);
},
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
genericType: 'georeference'
},
'georeference-street-address': {
title: _t('analyses.georeference.title'),
short_title: _t('analyses.georeference.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
checkIfEnabled: function (options) {
return checkIfExternalGeocodingIsEnabled(options);
},
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
genericType: 'georeference'
},
'georeference-admin-region': {
title: _t('analyses.georeference.title'),
short_title: _t('analyses.georeference.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
checkIfEnabled: function (options) {
return checkIfInternalGeocodingIsEnabled(options);
},
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
genericType: 'georeference'
},
'intersection': {
title: _t('analyses.filter-intersection.title'),
short_title: _t('analyses.filter-intersection.short-title'),
FormModel: FilterIntersectionFormModel,
modalTitle: _t('components.modals.add-analysis.option-types.filter-intersection.title'),
modalDesc: _t('components.modals.add-analysis.option-types.filter-intersection.desc'),
modalCategory: 'data_transformation',
modalLink: 'https://carto.com/learn/guides/analysis/select-points-in-polygons/',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/intersection.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/filter-by-polygon.tpl'),
checkIfEnabled: function (options) {
return options.queryGeometryModel.isPolygon();
}
},
'kmeans': {
title: _t('analyses.kmeans.title'),
short_title: _t('analyses.kmeans.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/kmeans-form-model'),
modalTitle: _t('components.modals.add-analysis.option-types.kmeans.title'),
modalDesc: _t('components.modals.add-analysis.option-types.kmeans.desc'),
modalCategory: 'analyze_predict',
modalLink: 'https://carto.com/learn/guides/analysis/calculate-clusters-of-points',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/kmeans.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/kmeans.tpl'),
animationParams: {
method: _t('analyses-onboarding.placeholders.clusters')
}
},
'line-to-single-point': {
title: _t('analyses.connect-with-lines.title'),
short_title: _t('analyses.connect-with-lines.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/connect-with-lines-form-model'),
modalTitle: _t('components.modals.add-analysis.option-types.connect-with-lines.title'),
modalDesc: _t('components.modals.add-analysis.option-types.connect-with-lines.desc'),
modalCategory: 'data_transformation',
modalLink: 'https://carto.com/learn/guides/analysis/create-lines-from-points/',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/connect-with-lines.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/connect-with-lines.tpl'),
genericType: 'connect-with-lines'
},
'line-source-to-target': {
title: _t('analyses.connect-with-lines.title'),
short_title: _t('analyses.connect-with-lines.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/connect-with-lines-form-model'),
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/connect-with-lines.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/connect-with-lines.tpl'),
genericType: 'connect-with-lines'
},
'line-sequential': {
title: _t('analyses.connect-with-lines.title'),
short_title: _t('analyses.connect-with-lines.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/connect-with-lines-form-model'),
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/connect-with-lines.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/connect-with-lines.tpl'),
genericType: 'connect-with-lines'
},
'merge': {
title: _t('analyses.merge.title'),
short_title: _t('analyses.merge.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/merge-form-model'),
modalTitle: _t('components.modals.add-analysis.option-types.merge.title'),
modalDesc: _t('components.modals.add-analysis.option-types.merge.desc'),
ModalModel: MergeAnalysisOptionModel,
modalCategory: 'create_clean',
modalLink: 'https://carto.com/learn/guides/analysis/add-columns-from-second-dataset/',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/merge.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/join-two-columns.tpl')
},
'moran': {
title: _t('analyses.moran-cluster.title'),
short_title: _t('analyses.moran-cluster.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/moran-form-model'),
modalTitle: _t('components.modals.add-analysis.option-types.moran-cluster.title'),
modalDesc: _t('components.modals.add-analysis.option-types.moran-cluster.desc'),
modalCategory: 'analyze_predict',
modalLink: 'https://carto.com/learn/guides/analysis/detect-outliers-and-clusters',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/moran.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/outliers.tpl')
},
'routing-sequential': {
title: _t('analyses.routing.title'),
short_title: _t('analyses.routing.short-title'),
FormModel: FallbackFormModel,
checkIfEnabled: function (options) {
return checkIfRoutingIsEnabled(options);
}
},
'routing-to-layer-all-to-all': {
title: _t('analyses.routing.title'),
short_title: _t('analyses.routing.short-title'),
FormModel: FallbackFormModel,
checkIfEnabled: function (options) {
return checkIfRoutingIsEnabled(options);
}
},
'routing-to-single-point': {
title: _t('analyses.routing.title'),
short_title: _t('analyses.routing.short-title'),
FormModel: FallbackFormModel,
checkIfEnabled: function (options) {
return checkIfRoutingIsEnabled(options);
}
},
'sampling': {
title: _t('analyses.sampling.title'),
short_title: _t('analyses.sampling.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/sampling-form-model'),
modalTitle: _t('components.modals.add-analysis.option-types.sampling.title'),
modalDesc: _t('components.modals.add-analysis.option-types.sampling.desc'),
modalCategory: 'data_transformation',
modalLink: 'https://carto.com/learn/guides/analysis/subsample-percent-of-rows',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/sampling.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/sampling.tpl'),
animationParams: {
percentage: _t('analyses-onboarding.placeholders.percentage')
},
genericType: 'sampling'
},
'spatial-markov-trend': {
title: _t('analyses.spatial-markov-trend.title'),
short_title: _t('analyses.spatial-markov-trend.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/spatial-markov-trend-model'),
modalTitle: _t('components.modals.add-analysis.option-types.spatial-markov-trend.title'),
modalDesc: _t('components.modals.add-analysis.option-types.spatial-markov-trend.desc'),
modalCategory: 'analyze_predict',
modalLink: 'https://carto.com/learn/guides/analysis/predict-trends-and-volatility',
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/spatial-markov-trend.tpl'),
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/predict-trends.tpl')
},
'trade-area': {
title: _t('analyses.area-of-influence.title'),
short_title: _t('analyses.area-of-influence.short-title'),
FormModel: AreaOfInfluenceFormModel,
checkIfEnabled: function (options) {
if (!options.configModel) throw new Error('configModel is required');
return !!options.configModel.dataServiceEnabled('isolines');
},
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/area-of-influence.tpl'),
genericType: 'area-of-influence'
},
'weighted-centroid': {
title: _t('analyses.centroid.title'),
short_title: _t('analyses.centroid.short-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/centroid-form-model'),
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/centroid.tpl'),
genericType: 'centroid'
},
'closest': {
title: _t('editor.layers.analysis-form.find-nearest.workflow-title'),
short_title: _t('editor.layers.analysis-form.find-nearest.workflow-title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/closest-form-model'),
modalTitle: _t('editor.layers.analysis-form.find-nearest.modal-title'),
modalDesc: _t('editor.layers.analysis-form.find-nearest.modal-desc'),
modalCategory: 'analyze_predict',
modalLink: 'https://carto.com/learn/guides/analysis/find-nearest',
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/closest.tpl'),
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/closest.tpl')
},
'deprecated-sql-function': {
title: _t('editor.layers.analysis-form.deprecated-sql-function.title'),
short_title: _t('editor.layers.analysis-form.deprecated-sql-function.title'),
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/deprecated-sql-function-form-model'),
modalTitle: _t('components.modals.add-analysis.option-types.deprecated-sql-function.title'),
modalDesc: _t('components.modals.add-analysis.option-types.deprecated-sql-function.desc'),
modalCategory: 'data_transformation',
ModalModel: DeprecatedSQLFunctionOptionModel,
checkIfEnabled: function (options) {
return options.userModel.featureEnabled('deprecated-sql-function');
}
}
};
var checkIfInternalGeocodingIsEnabled = function (options) {
if (!options.configModel) throw new Error('configModel is required');
return isDataServicesReady(options.configModel) && !!options.configModel.dataServiceEnabled('geocoder_internal');
};
var checkIfRoutingIsEnabled = function (options) {
if (!options.configModel) throw new Error('configModel is required');
return isDataServicesReady(options.configModel) && !!options.configModel.dataServiceEnabled('routing');
};
var checkIfExternalGeocodingIsEnabled = function (options) {
if (!options.configModel) throw new Error('configModel is required');
return isDataServicesReady(options.configModel) && !!options.configModel.dataServiceEnabled('hires_geocoder');
};
var checkIfDataObservatoryIsEnabled = function (options) {
if (!options.configModel) throw new Error('configModel is required');
return isDataServicesReady(options.configModel) && !!options.configModel.dataServiceEnabled('data_observatory');
};
var definitionByType = function (m) {
var type = 'unknown';
if (typeof m === 'string' && m !== '') {
type = m;
} else if (m && m.get) {
type = m.get('type');
if (type === 'data-observatory-measure' && m.has('measurement')) {
var measurement = m.get('measurement').toLowerCase().split(' ').join('-');
if (_.contains(DAO_TYPES, measurement)) {
var measurementType = _.clone(MAP[type]);
measurementType.title = _t('analyses.data-observatory-measure.' + measurement);
return measurementType;
}
}
}
return MAP[type] || {
title: _t('analyses.' + type),
short_title: _t('analyses.' + type),
modalLink: '',
FormModel: FallbackFormModel
};
};
var isDataServicesReady = function (configModel) {
return DataServicesApiCheck.get(configModel).isReady();
};
var _isAnalysisValidByType = function (type, options) {
var analysis = MAP[type];
return analysis && analysis.checkIfEnabled ? analysis.checkIfEnabled(options) : true;
};
var DATA_OBSERVATORY_OLD = 'data-observatory-measure';
module.exports = {
MAP: MAP,
findFormModelByType: function (type) {
var isDummy = MAP[type] && MAP[type].dummy;
return camshaftReference.hasType(type) || isDummy
? definitionByType(type).FormModel
: UnknownTypeFormModel;
},
getAnalysesByModalCategory: function (category, options) {
return _.reduce(MAP, function (memo, item, analysisType) {
if (analysisType === DATA_OBSERVATORY_OLD) {
return memo;
}
if (item.modalCategory === category) {
if (_isAnalysisValidByType(analysisType, options)) {
var obj = {
nodeAttrs: {
type: analysisType
},
title: item.modalTitle,
desc: item.modalDesc,
link: item.modalLink
};
if (item.ModalModel) {
obj.Model = item.ModalModel;
}
if (item.dummy) {
obj.dummy = item.dummy;
}
memo.push(obj);
}
}
return memo;
}, []);
},
isAnalysisValidByType: function (type, options) {
return _isAnalysisValidByType(type, options);
},
getAnalysisByType: function (type) {
return MAP[type];
},
title: function (model) {
return definitionByType(model).title;
},
short_title: function (model) {
return definitionByType(model).short_title;
},
link: function (model) {
return definitionByType(model).modalLink;
}
};

View File

@@ -0,0 +1,53 @@
var _ = require('underscore');
var Backbone = require('backbone');
var syncAbort = require('./backbone/sync-abort');
/**
* Analysis definition model.
* Points to a node that is the head of the particular analysis.
*/
module.exports = Backbone.Model.extend({
/**
* @override {Backbone.prototype.sync} abort ongoing request if there is any
*/
sync: syncAbort,
initialize: function (attrs, opts) {
if (!opts.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
this.USER_SAVED = false; // flag to indicate if the user saved the node and avoid triggering the onboarding model if they didn't
this._analysisDefinitionNodesCollection = opts.analysisDefinitionNodesCollection;
},
parse: function (r, opts) {
// If parse is called on a new model it have not yet called initialize and thus don't have the collection
var nodesCollection = this._analysisDefinitionNodesCollection || opts.analysisDefinitionNodesCollection;
// Merge and parse are used to sync all nodes in a definition with the backend response
nodesCollection.add(r.analysis_definition, {silent: true, merge: true, parse: true});
var attrs = _.omit(r, 'analysis_definition');
attrs.node_id = r.analysis_definition.id;
return attrs;
},
toJSON: function () {
return {
id: this.id,
analysis_definition: this.getNodeDefinitionModel().toJSON()
};
},
containsNode: function (other) {
var nodeDefModel = this.getNodeDefinitionModel();
return !!(nodeDefModel && nodeDefModel.containsNode(other));
},
getNodeDefinitionModel: function () {
return this._analysisDefinitionNodesCollection.get(this.get('node_id'));
}
});

View File

@@ -0,0 +1,327 @@
var _ = require('underscore');
var Backbone = require('backbone');
var camshaftReference = require('./camshaft-reference');
var QueryGeometryModel = require('./query-geometry-model');
var QuerySchemaModel = require('./query-schema-model');
var nodeIds = require('builder/value-objects/analysis-node-ids');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var QueryRowsCollection = require('./query-rows-collection');
var fetchAllQueryObjects = require('builder/helpers/fetch-all-query-objects');
var layerColors = require('builder/data/layer-colors');
var REQUIRED_OPTS = [
'configModel'
];
/**
* Base model for an analysis definition node.
* May point to one or multiple nodes in turn (referenced by ids).
*
*
* Analysis-definition-node-model
* ├── Query-Schema-Model
* ├── Query-Geometry-Model
* └── Query-Rows-Collection
*/
module.exports = Backbone.Model.extend({
initialize: function (attrs, opts) {
if (!this.id) throw new Error('id is required');
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
var modelOpts = {
configModel: this._configModel,
userModel: opts.userModel
};
var simpleGeom = this.get('simple_geom');
this.querySchemaModel = new QuerySchemaModel(undefined, modelOpts);
this.queryGeometryModel = new QueryGeometryModel({
simple_geom: simpleGeom,
status: simpleGeom ? 'fetched' : 'unfetched'
}, modelOpts);
this.queryRowsCollection = new QueryRowsCollection([], {
configModel: this._configModel,
userModel: opts.userModel,
tableName: '',
querySchemaModel: this.querySchemaModel
});
this.listenTo(this.queryGeometryModel, 'change:simple_geom', this._onGeometryTypeChanged);
},
fetchQueryObjects: function () {
if (this.get('status') === 'ready' && !this.get('error')) {
fetchAllQueryObjects({
queryGeometryModel: this.queryGeometryModel,
querySchemaModel: this.querySchemaModel,
queryRowsCollection: this.queryRowsCollection
});
}
},
/**
* @override {Backbone.prototype.parse} flatten the provided analysis data and create source nodes if there are any.
*/
parse: function (r, opts) {
// Allow omitting type when merging into an existing node
var sourceNames = (!r.type && opts.merge) ? [] : camshaftReference.getSourceNamesForAnalysisType(r.type);
var parsedParams = _.reduce(r.params, function (memo, val, name) {
var sourceName = sourceNames[sourceNames.indexOf(name)];
if (sourceName) {
this.collection.add(val, opts);
memo[name] = val.id;
} else {
memo[name] = val;
}
return memo;
}, {}, this);
var parsedOptions = _.reduce(r.options, function (memo, val, name) {
memo[name] = val;
return memo;
}, {}, this);
return _.defaults(
_.omit(r, 'params', 'options'),
parsedOptions,
parsedParams
);
},
/**
* @override {Backbone.prototype.toJSON} unflatten the internal structure to the expected nested JSON data structure.
* @param {Object} options
* @param {Boolean} options.skipOptions - Add or not analysis options to the definition
*/
toJSON: function (options) {
options = options || {};
var analysisParams = [];
var paramsforJSON = {};
var paramsForType = camshaftReference.paramsForType(this.get('type'));
// Undo the parsing of the params previously done in .parse() (when model was created)
for (var name in paramsForType) {
var param = paramsForType[name];
var val = this.get(name);
var includeParamInJSON = true;
if (param.type === 'node') {
var node = this.collection.get(val);
if (node) {
val = node.toJSON(options);
} else if (param.optional) { // Node has no value but is optional. We don't serialize it.
includeParamInJSON = false;
} else { // Node has no value and is required. Throw error.
throw new Error('no node found for param "' + name + '" with id "' + val + '" in node ' + JSON.stringify(this.attributes));
}
}
if (includeParamInJSON) {
paramsforJSON[name] = val;
analysisParams.push(name);
}
}
var json = {
id: this.get('id'),
type: this.get('type'),
params: paramsforJSON
};
// style_history is not sent at all (neither inside params nor options). It is always set by the backend.
var optionsAttrs = _.omit(this.attributes, analysisParams.concat('id', 'type', 'status', 'style_history'));
if (!(options.skipOptions || _.isEmpty(optionsAttrs))) {
json['options'] = optionsAttrs;
}
return json;
},
/**
* @override {Backbone.prototype.isNew} for this.destroy() to work (not try to send DELETE request)
*/
isNew: function () {
return true;
},
isReadOnly: function () {
// By default, any node should be read only by default, except the source node (with exceptions)
return true;
},
isCustomQueryApplied: function () {
// By default, any node shouldn't have a custom query applied, except the source node
return false;
},
canBeDeletedByUser: function () {
return this.hasPrimarySource();
},
hasFailed: function () {
return this.get('status') === 'failed';
},
isDone: function () {
return this.get('status') === 'ready';
},
destroy: function () {
if (this.querySchemaModel) {
this.querySchemaModel.destroy();
this.querySchemaModel = null;
}
return Backbone.Model.prototype.destroy.apply(this, arguments);
},
/**
* Creates a new node that have same params as the current node, with only the id differing.
* @param {String} newId - e.g. 'b1'
* @return {Object} instance of analysis-definition-node-model
*/
clone: function (newId) {
if (!newId) throw new Error('newId is required');
if (!_.isString(newId) || newId.length < 2) throw new Error('newId is required as a non-empty string');
if (newId === this.id) throw new Error('newId must be different from current id, ' + this.id);
var attrs = this.toJSON();
attrs.id = newId;
return this.collection.add(attrs);
},
linkedListBySameLetter: function () {
var list = [this];
var prev = this;
var letter = this.letter();
while (true) {
var current = prev.getPrimarySource();
if (current && current.letter() === letter) {
list.push(current);
prev = current;
} else {
break;
}
}
return list;
},
/**
* @return {Boolean, null} null if the geoemtry type of this node is not known,
* this is to indicate that the query model is not ready yet, up to caller to make the call or not
*/
isValidAsInputForType: function (analysisType) {
if (analysisType === 'source') return false;
var geometry = this.queryGeometryModel.get('simple_geom');
return geometry
? camshaftReference.isValidInputGeometryForType(geometry, analysisType)
: null;
},
containsNode: function (other) {
if (!other) return false;
return this.id === other.id || this._sourcesContains(other);
},
hasPrimarySource: function () {
return !!this.getPrimarySource();
},
hasSecondarySource: function () {
return !!this.getSecondarySource();
},
getPrimarySource: function () {
return this.collection.get(this._getPrimarySourceId());
},
getSecondarySource: function () {
// Assumption: there are only 1-2 sources, so secondary is the one that's not the primary
var primarySourceId = this._getPrimarySourceId();
var secondarySourceId = _.find(this.sourceIds(), function (sourceId) {
return sourceId !== primarySourceId;
});
return this.collection.get(secondarySourceId);
},
// Default query by default is the query already applied in the node.
// For source node is totally different
getDefaultQuery: function () {
return this.get('query');
},
/**
* @return {Array} e.g. ['c3', 'b2']
*/
sourceIds: function () {
return _.map(this._sourceNames(), function (sourceName) {
return this.get(sourceName);
}, this);
},
changeSourceIds: function (currentId, newId, silenty) {
this._sourceNames().forEach(function (sourceName) {
if (this.get(sourceName) === currentId) {
this.set(sourceName, newId, { silent: silenty });
}
}, this);
},
isSourceType: function () {
return this.get('type') === 'source';
},
letter: function () {
return nodeIds.letter(this.id);
},
getColor: function () {
var letter = nodeIds.letter(this.id);
return layerColors.getColorForLetter(letter);
},
getStyleHistoryForLayer: function (layer_id) {
var styleHistory = this.get('style_history');
return styleHistory && styleHistory[layer_id];
},
// Private methods
_sourcesContains: function (other) {
var primarySource = this.getPrimarySource();
var secondarySource = this.getSecondarySource();
return !!(
(primarySource && primarySource.containsNode(other)) ||
(secondarySource && secondarySource.containsNode(other))
);
},
_getPrimarySourceId: function () {
var primarySourceName = this.get('primary_source_name') || this._sourceNames()[0];
return this.get(primarySourceName);
},
/**
* @return {Array} e.g. ['polygons_source', 'points_source']
*/
_sourceNames: function () {
return camshaftReference.getSourceNamesForAnalysisType(this.get('type'));
},
_onGeometryTypeChanged: function (model, value) {
this.set('simple_geom', value);
}
});

View File

@@ -0,0 +1,94 @@
var AnalysisDefinitionNodeModel = require('./analysis-definition-node-model');
var TableModel = require('./table-model');
var SQLUtils = require('builder/helpers/sql-utils');
/**
* Case of a node model representing a source node.
* - It should provide new info about if the node is read only or not.
*
* Analysis-definition-node-source-model
* ├── Query-Schema-Model
* ├── Query-Geometry-Model
* └── Table-Model
*/
module.exports = AnalysisDefinitionNodeModel.extend({
defaults: {
status: 'ready'
},
/**
* @override AnalysisDefinitionNodeModel.prototype.initialize
*/
initialize: function (attrs, opts) {
if (!opts.userModel) throw new Error('userModel is required');
this._userModel = opts.userModel;
AnalysisDefinitionNodeModel.prototype.initialize.apply(this, arguments);
var query = this.get('query');
this.querySchemaModel.set({
query: query,
ready: true
}, { silent: true });
this.queryGeometryModel.set({
query: query,
ready: true
}, { silent: true });
// TODO: we should check if it is necessary to check if we have to overwrite the whole
// initialize or with this change is enough.
this.queryRowsCollection._tableName = this.get('table_name');
var tableData = opts.tableData || { name: this.get('table_name') };
this.tableModel = new TableModel(tableData, {
configModel: opts.configModel,
parse: true
});
},
getDefaultQuery: function () {
return SQLUtils.getDefaultSQL(
this.get('table_name'),
this.tableModel.getOwnerName(),
this._userModel.isInsideOrg()
);
},
isCustomQueryApplied: function () {
return !SQLUtils.isSameQuery(
this.querySchemaModel.get('query'),
this.getDefaultQuery()
);
},
isReadOnly: function () {
var isTableReadOnly = this.tableModel.isReadOnly(this._userModel);
var hasCustomQuery = this.isCustomQueryApplied();
return isTableReadOnly || hasCustomQuery;
},
getTableModel: function () {
return this.tableModel;
},
fetchTable: function () {
if (!this.tableModel.get('id')) {
this.tableModel.fetch();
}
},
setTableName: function (name) {
if (!name) throw new Error('name is required');
this.set('table_name', name);
this.tableModel.set('name', name);
this.queryRowsCollection.setTableName(name);
this.querySchemaModel.set({
status: 'unfetched',
query: this.getDefaultQuery()
});
}
});

View File

@@ -0,0 +1,103 @@
var _ = require('underscore');
var Backbone = require('backbone');
var AnalysisDefinitionNodeSourceModel = require('./analysis-definition-node-source-model');
var AnalysisDefinitionNodeModel = require('./analysis-definition-node-model');
var SQLUtils = require('builder/helpers/sql-utils');
var TableNameUtils = require('builder/helpers/table-name-utils');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'userModel',
'configModel'
];
/**
* Collection of analysis definitions nodes.
*/
module.exports = Backbone.Collection.extend({
initialize: function (models, opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._relatedTableData = opts.relatedTableData || [];
this._initBinds();
},
_initBinds: function () {
this.on('queryObjectsUpdated', this._onQueryObjectsUpdated);
},
addRelatedTableData: function (tableData) {
this._relatedTableData.push(tableData);
},
model: function (r, opts) {
var self = opts.collection;
opts = _.extend(
{
parse: true,
configModel: self._configModel,
userModel: self._userModel
},
opts
);
if (r.type === 'source') {
var tableData = _.find(self._relatedTableData, function (t) {
return TableNameUtils.isSameTableName(r.options.table_name, t.name, self._configModel.get('user_name'));
});
if (tableData) {
opts.tableData = tableData;
}
return new AnalysisDefinitionNodeSourceModel(r, opts);
} else {
return new AnalysisDefinitionNodeModel(r, opts);
}
},
createSourceNode: function (d) {
d = d || {};
if (!d.id) throw new Error('id is required');
if (!d.tableName) throw new Error('tableName is required');
return this.add({
id: d.id,
type: 'source',
params: {
query: d.query || SQLUtils.getDefaultSQL(d.tableName, TableNameUtils.getUsername(d.tableName), true)
},
options: {
table_name: d.tableName
}
});
},
_onQueryObjectsUpdated: function (model) {
this._fetchQueryObjectsIfOnTop(model);
},
_fetchQueryObjectsIfOnTop: function (model) {
if (this._isNodeOnTop(model)) {
model.fetchQueryObjects();
}
},
_isNodeOnTop: function (model) {
var currentPos = this.indexOf(model);
return currentPos === this._getTopNodeIndex();
},
_getTopNodeIndex: function () {
var nodeOnTop = this._getNodeOnTop();
var at = this.indexOf(nodeOnTop);
return at;
},
_getNodeOnTop: function () {
return this.last();
}
});

View File

@@ -0,0 +1,88 @@
var Backbone = require('backbone');
var nodeIds = require('builder/value-objects/analysis-node-ids');
var AnalysisDefinitionModel = require('./analysis-definition-model');
/**
* Collection of analysis definitions.
*/
module.exports = Backbone.Collection.extend({
model: function (d, opts) {
var self = opts.collection;
var m = new AnalysisDefinitionModel(d, {
parse: true,
collection: self, // used implicitly, for model to use collection.url()
analysisDefinitionNodesCollection: self._analysisDefinitionNodesCollection
});
return m;
},
initialize: function (models, options) {
if (!options.configModel) throw new Error('configModel is required');
if (!options.vizId) throw new Error('vizId is required');
if (!options.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
this._configModel = options.configModel;
this._vizId = options.vizId;
this._analysisDefinitionNodesCollection = options.analysisDefinitionNodesCollection;
},
url: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v3/viz/' + this._vizId + '/analyses';
},
findAnalysisThatContainsNode: function (nodeDefModel) {
return this.findWhere({node_id: nodeDefModel.id}) ||
this.find(function (m) {
return m.containsNode(nodeDefModel);
});
},
findAnalysisForLayer: function (layerDefModel) {
var layerLetter = layerDefModel.get('letter');
return this.find(function (m) {
var letter = nodeIds.letter(m.get('node_id'));
return letter === layerLetter;
});
},
newAnalysisForNode: function (nodeDefModel) {
return this.add({analysis_definition: nodeDefModel.toJSON()});
},
/**
* @param {object} layerDefModel - instance of an analysis-definition-node-model
* @return {jqXHR, undefined} A promise if created a new analysis
*/
saveAnalysisForLayer: function (layerDefModel) {
if (!layerDefModel) throw new Error('layerDefModel is required');
var nodeDefModel = layerDefModel.getAnalysisDefinitionNodeModel();
var analysisDefModel = this.findAnalysisForLayer(layerDefModel);
if (layerDefModel.isOwnerOfAnalysisNode(nodeDefModel)) {
if (analysisDefModel) {
analysisDefModel.set('node_id', nodeDefModel.id);
} else {
analysisDefModel = this.add({analysis_definition: nodeDefModel.toJSON()});
}
} else {
if (analysisDefModel) {
analysisDefModel.destroy(); // layer's node is owned by another layer, so the existing analysis should be deleted
}
}
this.each(function (analysisDefModel) {
if (analysisDefModel.containsNode(nodeDefModel)) {
analysisDefModel.save();
}
});
return analysisDefModel;
}
});

View File

@@ -0,0 +1,132 @@
var _ = require('underscore');
var Backbone = require('backbone');
var Utils = require('builder/helpers/utils');
require('backbone-model-file-upload');
module.exports = Backbone.Model.extend({
fileAttribute: 'filename',
defaults: {
type: '',
filename: '',
interval: 0,
progress: 0,
state: 'idle',
option: ''
},
_initBinds: function () {
this.bind('progress', function (progress) {
this.set({
progress: progress * 100,
state: 'uploading'
});
}, this);
this.bind('change:filename change:url', function () {
if (this.get('state') === 'error') {
this.set({ state: 'idle' });
this.unset('get_error_text', { silent: true });
}
}, this);
this.bind('error invalid', function (m, d) {
this.set({
state: 'error',
error_code: (d && d.error_code) || '',
get_error_text: {
title: _t('componentes.data.asset-model.invalid-import'),
what_about: (d && d.msg) || ''
}
}, { silent: true });
// We need this, if not validate will run again and again and again... :(
this.trigger('change');
}, this);
},
validate: function (attrs) {
if (!attrs) {
return;
}
if (attrs.type === 'filename') { // Number of files
if (attrs.filename && attrs.filename.length) {
return {
msg: _t('componentes.data.asset-model.one-file-per-upload')
};
}
// File extension
var name = attrs.filename.name;
var ext = name.substr(name.lastIndexOf('.') + 1);
if (ext) {
ext = ext.toLowerCase();
}
if (!_.contains(['jpg', 'png', 'gif', 'svg'], ext)) {
return {
msg: _t('componentes.data.asset-model.invalid-extension')
};
}
}
if (attrs.type === 'url') { // Valid URL?
if (!Utils.isURL(attrs.url)) {
return {
msg: _t('componentes.data.asset-model.invalid-url')
};
}
}
},
isValid: function () {
return (this.get('filename') || this.get('url')) && this.get('state') !== 'error';
},
upload: function () {
var self = this;
var options = {
kind: this.get('kind')
};
if (this.get('type') === 'file') {
options.filename = this.get('filename');
} else if (this.get('type') === 'url') {
options.filename = this.get('filename');
}
this.xhr = this.save(options, {
success: function (m) {
m.set('state', 'uploaded');
},
error: function (m, msg) {
var response;
var message = _t('componentes.data.asset-model.connection-error');
if (msg && msg.status === 429) {
response = JSON.parse(msg.responseText);
message = response.error;
} else if (msg && msg.status === 400) {
response = JSON.parse(msg.responseText);
message = response.error;
}
self.set({
state: 'error',
get_error_text: { title: _t('componentes.data.asset-model.error'), what_about: message }
});
},
complete: function () {
delete self.xhr;
}
});
},
stopUpload: function () {
if (this.xhr) this.xhr.abort();
}
});

View File

@@ -0,0 +1,40 @@
var Backbone = require('backbone');
var AssetModel = require('./asset-model');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'configModel',
'userModel'
];
module.exports = Backbone.Collection.extend({
model: AssetModel,
url: function (method) {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('asset', method);
return baseUrl + '/api/' + version + '/users/' + this._userModel.get('id') + '/assets';
},
initialize: function (models, opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
},
parse: function (resp, xhr) {
return resp.assets;
},
selectAll: function () {
this.each(function (mdl) {
mdl.set('state', 'selected');
});
},
deselectAll: function (m) {
this.each(function (mdl) {
if (mdl !== m && mdl.get('state') === 'selected') {
mdl.set('state', '');
}
});
}
});

View File

@@ -0,0 +1,111 @@
const $ = require('jquery');
const _ = require('underscore');
/**
* Network Interceptors
*
* Tool to easily intercept network responses in order
* to override or add behaviours on top of the usual ones
*/
function NetworkResponseInterceptor () {
this._originalAjax = $.ajax;
this._successInterceptors = [];
this._errorInterceptors = [];
this._urlPatterns = [];
}
NetworkResponseInterceptor.prototype.start = function () {
let onRequestStart = this._onRequestStart.bind(this);
this._onRequestStart = onRequestStart;
$.ajax = onRequestStart;
};
NetworkResponseInterceptor.prototype.stop = function () {
$.ajax = this._originalAjax;
};
NetworkResponseInterceptor.prototype.addURLPattern = function (pattern) {
this._urlPatterns.push(pattern);
};
NetworkResponseInterceptor.prototype.addSuccessInterceptor = function (interceptorFn) {
this._successInterceptors.push(interceptorFn);
};
NetworkResponseInterceptor.prototype.addErrorInterceptor = function (interceptorFn) {
this._errorInterceptors.push(interceptorFn);
};
NetworkResponseInterceptor.prototype._shouldListenToRequest = function (url) {
let patternChecks = this._urlPatterns.map(function (pattern) {
return url.indexOf(pattern) > -1;
});
return _.some(patternChecks);
};
NetworkResponseInterceptor.prototype._onRequestStart = function (url, options) {
let requestUrl = null;
let requestOptions = {};
if (typeof url === 'string') {
requestUrl = url;
} else if (typeof url === 'object' && url.url) {
requestUrl = url.url;
}
if (options) {
requestOptions = options;
} else if (typeof url === 'object') {
requestOptions = url;
}
if (!this._shouldListenToRequest(requestUrl)) {
return this._originalAjax.apply(this, arguments);
}
if (requestOptions.error || requestOptions.success) {
const errorCallback = requestOptions.error;
const successCallback = requestOptions.success;
const interceptErrorFn = function () {
this._applyErrorInterceptors.apply(this, arguments);
errorCallback && errorCallback.apply(this, arguments);
};
const interceptSuccessFn = function () {
this._applySuccessInterceptors.apply(this, arguments);
successCallback && successCallback.apply(this, arguments);
};
requestOptions.error = interceptErrorFn.bind(this);
requestOptions.success = interceptSuccessFn.bind(this);
}
return this._originalAjax(requestUrl, requestOptions);
};
NetworkResponseInterceptor.prototype._applyErrorInterceptors = function (xhr, textStatus, errorThrown) {
if (!this._errorInterceptors.length) {
return;
}
this._errorInterceptors.map(function (interceptorFunction) {
interceptorFunction(xhr, textStatus, errorThrown);
});
};
NetworkResponseInterceptor.prototype._applySuccessInterceptors = function (response, textStatus, xhr) {
if (!this._successInterceptors.length) {
return;
}
this._successInterceptors.map(function (interceptorFunction) {
interceptorFunction(response, textStatus, xhr);
});
};
module.exports = new NetworkResponseInterceptor();
module.exports.NetworkResponseInterceptor = NetworkResponseInterceptor;

View File

@@ -0,0 +1,61 @@
/**
* 403 Forbidden Network Error Interceptor
*
* This interceptor redirects 403 errors
* to their corresponding routes
*/
const LOGIN_ROUTE = '/login?error=session_expired';
const SESSION_EXPIRED = 'session_expired';
const MFA_REQUIRED = 'mfa_required';
const MFA_ROUTE = '/multifactor_authentication';
const MAINTENANCE_MODE = 'maintenance_mode';
const MAINTENANCE_MODE_ROUTE = '/maintenance_mode';
const LOCKOUT = 'lockout';
const LOCKOUT_ROUTE = '/lockout';
const subdomainMatch = /https?:\/\/([^.]+)/;
const redirectRoutes = {
[SESSION_EXPIRED]: LOGIN_ROUTE,
[MFA_REQUIRED]: MFA_ROUTE,
[MAINTENANCE_MODE]: MAINTENANCE_MODE_ROUTE,
[LOCKOUT]: LOCKOUT_ROUTE
};
module.exports = function (username) {
return function (xhr, textStatus, errorThrown) {
if (xhr.status !== 403) return;
const error = xhr.responseJSON && xhr.responseJSON.error;
const redirectRoute = redirectRoutes[error];
if (!redirectRoute) {
return;
}
URLFunctions.redirectTo(
URLFunctions.getRedirectURL(window.location.origin, redirectRoute, username)
);
};
};
const URLFunctions = {
getRedirectURL: function getRedirectURL (currentURLOrigin, route, username) {
if (!username) {
return currentURLOrigin + route;
}
const newURL = currentURLOrigin.replace(subdomainMatch, function () {
return username;
});
return `${window.location.protocol}//${newURL}${route}`;
},
redirectTo: function redirectTo (url) {
window.location = url;
}
};
module.exports.URLFunctions = URLFunctions;

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,102 @@
var _ = require('underscore');
var Backbone = require('backbone');
var ImportsModel = require('./imports-model');
var POLL_TIMER = 30000;
/**
* Imports collection
*
* If it is fetched, it will add the import
*
*/
module.exports = Backbone.Collection.extend({
initialize: function (models, 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;
},
model: function (attrs, opts) {
return new ImportsModel(attrs, {
userModel: opts.collection._userModel,
configModel: opts.collection._configModel
});
},
url: function (method) {
var version = this._configModel.urlVersion('import');
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/' + version + '/imports';
},
parse: function (r) {
var self = this;
if (r.imports.length === 0) {
this.destroyCheck();
} else {
_.each(r.imports, function (id) {
// Check if that import exists...
var imports = self.filter(function (mdl) {
return mdl._importModel.get('item_queue_id') === id;
});
if (imports.length === 0) {
var importsModel = new ImportsModel({
id: id
}, {
userModel: self._userModel,
configModel: self._configModel
});
self.add(importsModel);
}
});
}
return this.models;
},
canImport: function () {
var importQuota = this._userModel.getMaxConcurrentImports();
var total = this.size();
var finished = 0;
this.each(function (m) {
if (m.hasFailed() || m.hasCompleted()) {
++finished;
}
});
return (total - finished) < importQuota;
},
pollCheck: function (i) {
if (this.pollTimer) {
return;
}
var self = this;
this.pollTimer = setInterval(function () {
self.fetch();
}, POLL_TIMER || 2000);
this.fetch();
},
destroyCheck: function () {
clearInterval(this.pollTimer);
delete this.pollTimer;
},
failedItems: function () {
return this.filter(function (item) {
return item.hasFailed();
});
}
});

View File

@@ -0,0 +1,100 @@
var Backbone = require('backbone');
var ImportsCollection = require('./background-importer-imports-collection');
var POLLINGS_TIMER = 3000;
/**
* Background polling default model
*
*/
module.exports = Backbone.Model.extend({
defaults: {
showSuccessDetailsButton: true,
importsPolling: false // enable imports polling
},
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.importsCollection = opts.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);
},
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 () {
var self = this;
// Don't start pollings inmediately
setTimeout(function () {
if (self.get('importsPolling')) {
self.importsCollection.pollCheck();
}
}, POLLINGS_TIMER);
},
_onImportsStateChange: function () {},
clean: function () {
this.importsCollection.unbind(null, null, this);
}
});

View File

@@ -0,0 +1,27 @@
var _ = require('underscore');
var Poller = require('./poller');
var ImportModelPoller = function (model) {
var POLLING_INTERVAL = 2000; // Interval time between poll checkings
var POLLING_INTERVAL_MULTIPLIER = 2.5; // Multiply interval by this number
var POLLING_REQUESTS_BEFORE_INTERVAL_CHANGE = 30; // Max tries until interval change
var options = {
interval: function (numberOfRequests) {
if (numberOfRequests >= POLLING_REQUESTS_BEFORE_INTERVAL_CHANGE) {
return POLLING_INTERVAL * POLLING_INTERVAL_MULTIPLIER;
}
return POLLING_INTERVAL;
},
stopWhen: function (model) {
var state = model.get('state');
return (state === 'complete' || state === 'failure');
}
};
Poller.call(this, model, options);
};
ImportModelPoller.prototype = _.extend({}, Poller.prototype);
module.exports = ImportModelPoller;

View File

@@ -0,0 +1,162 @@
var _ = require('underscore');
var Backbone = require('backbone');
var ImportModelPoller = require('./import-model-poller');
var SynchronizationModel = require('builder/data/synchronization-model');
/**
* New import model that controls
* the state of an import
*
*/
module.exports = Backbone.Model.extend({
idAttribute: 'item_queue_id',
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
this._initBinds();
this.poller = new ImportModelPoller(this);
},
urlRoot: function () {
var version = this._configModel.urlVersion('import');
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/' + version + '/imports';
},
_initBinds: function () {
this.bind('change:item_queue_id', this._checkQueueId, this);
},
createImport: function (data) {
var d = this._prepareData(data);
this[d.interval === 0 ? '_createRegularImport' : '_createSyncImport'](d);
},
_checkQueueId: function () {
if (this.get('item_queue_id')) {
this.pollCheck();
}
},
_prepareData: function (data) {
var d = {
create_vis: data.create_vis,
privacy: data.privacy
};
var type = data.type;
if (type !== 'remote') {
_.extend(d, {
type_guessing: data.type_guessing,
content_guessing: data.content_guessing,
interval: data.interval
});
}
var service = data.service_name;
if (type === 'url') {
_.extend(d, {
url: data.value
});
}
if (type === 'remote') {
_.extend(d, {
type: 'remote',
interval: null,
remote_visualization_id: data.remote_visualization_id,
create_vis: false,
value: data.value
});
}
if (type === 'sql') {
_.extend(d, {
table_name: data.table_name,
sql: data.value
});
}
if (type === 'duplication') {
_.extend(d, {
table_name: data.table_name,
table_copy: data.value
});
}
if (type === 'service') {
// If service is Twitter, service_item_id should be
// sent stringified
var service_item_id = (service === 'twitter_search') ? JSON.stringify(data.service_item_id) : data.service_item_id;
if (data.user_defined_limits) {
d.user_defined_limits = data.user_defined_limits;
}
_.extend(d, {
value: data.value,
service_name: data.service_name,
service_item_id: service_item_id
});
}
return d;
},
_createSyncImport: function (d) {
var self = this;
this._synchronizationModel = new SynchronizationModel(d, {
configModel: this._configModel
});
this._synchronizationModel.save(null, {
success: function (m) {
self.set('item_queue_id', m.get('data_import').item_queue_id);
},
error: function (mdl, r, opts) {
self._setErrorState(r);
}
});
},
_createRegularImport: function (d) {
var self = this;
this.save(d, {
error: function (mdl, r, opts) {
self._setErrorState(r);
}
});
},
_setErrorState: function (r) {
var msg;
try {
msg = r && JSON.parse(r.responseText).errors.imports;
} catch (err) {
msg = _t('data.import-model.error-starting-import');
}
this.set({
state: 'failure',
get_error_text: {
title: _t('data.import-model.error-title'),
what_about: msg
}
});
},
pollCheck: function () {
this.poller.start();
},
destroyCheck: function () {
this.poller.stop();
}
});

View File

@@ -0,0 +1,276 @@
var _ = require('underscore');
var Backbone = require('backbone');
var ImportModel = require('./import-model');
var UploadModel = require('builder/data/upload-model');
var VisDefinitionModel = require('builder/data/vis-definition-model');
var PermissionModel = require('builder/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 VisDefinitionModel(attrs, {
configModel: this._configModel
});
vis.permission = new PermissionModel({
// TODO: check that this works in builder
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,83 @@
var _ = 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();
*
*/
var Poller = function (model, opts) {
this.model = model;
this.numberOfRequests = 0;
this.polling = false;
this.interval = opts.interval;
if (typeof this.interval !== 'function') {
this.interval = function () { return opts.interval; };
}
this.stopWhen = opts.stopWhen;
this.error = opts.error;
this.autoStart = opts.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 () {
var self = this;
if (!self.polling) {
self.polling = true;
self.model.fetch({
success: function () {
self.polling = false;
self.numberOfRequests++;
if (self._continuePolling()) {
self._scheduleFetch();
}
},
error: function (e) {
_.isFunction(self.error) && self.error(self.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,25 @@
var PROVIDER_LEAFLET = 'leaflet';
var PROVIDER_GOOGLE_MAPS = 'googlemaps';
var CATEGORY_GOOGLE_MAPS = 'GMaps';
var LAYER_TYPE_GOOGLE_MAPS = 'GMapsBase';
var LAYER_TYPE_TILED = 'Tiled';
module.exports = {
getProvider: function (layerType) {
if (layerType === LAYER_TYPE_GOOGLE_MAPS) {
return PROVIDER_GOOGLE_MAPS;
}
return PROVIDER_LEAFLET;
},
getLayerType: function (category) {
if (this.isGoogleMapsCategory(category)) {
return LAYER_TYPE_GOOGLE_MAPS;
}
return LAYER_TYPE_TILED;
},
isGoogleMapsCategory: function (category) {
return category === CATEGORY_GOOGLE_MAPS;
}
};

View File

@@ -0,0 +1,169 @@
var _ = require('underscore');
var camshaftReference = require('camshaft-reference').getVersion('latest');
var DefaultCartography = require('./default-cartography.json');
var SOURCE_NAMES_MAP = {}; // string -> array, e.g. {'intersection': ['source', 'target']}
var DEFAULT_MISSING_PARAM_VALUES = [undefined, null, '', NaN];
module.exports = {
hasType: function (type) {
return !!camshaftReference.analyses[type];
},
paramsForType: function (type) {
if (!this.hasType(type)) throw new Error('no analysis params found for type: ' + type);
return _.clone(camshaftReference.analyses[type].params);
},
/**
* Validate raw form attrs
* @param {Object} formAttrs - e.g. {type: 'buffer', source: 'a0', radius: 'meh'}
* @return {Object, undefined} returns an object with keys as faulty input, e.g. {radius: 'invalid-value'} for above
*/
validate: function (formAttrs) {
var errors = {};
var parsedAttrs = this.parse(formAttrs);
var params = this.paramsForType(formAttrs.type);
for (var name in params) {
var param = params[name];
var val = parsedAttrs[name];
if (!param.optional || val !== undefined) {
switch (param.type) {
case 'node':
if (!val) {
errors[name] = _t('data.analysis-definition-node-model.validation.invalid-source');
}
break;
case 'number':
if (isNaN(val)) {
errors[name] = _t('data.analysis-definition-node-model.validation.invalid-value');
}
break;
case 'enum':
if (!_.contains(param.values, val)) {
errors[name] = _t('data.analysis-definition-node-model.validation.invalid-enum', {expectedValues: JSON.stringify(param.values)});
}
break;
default:
if (val === null) {
errors[name] = _t('data.analysis-definition-node-model.validation.missing-required');
}
}
}
}
if (!_.isEmpty(errors)) {
return errors;
}
},
/**
* Get type-parsed attrs from the raw form attrs
* @param {Object} formAttrs - e.g. {type: 'buffer', source: 'a0 ', radius: '123', dissolved: 'false'}
* @return {Object} e.g. {type: 'buffer', source: 'a0', radius: 123, dissolved: false}
*/
parse: function (formAttrs) {
var parsedAttrs = _.extend({}, formAttrs);
var params = this.paramsForType(formAttrs.type);
for (var name in params) {
var param = params[name];
var val = parsedAttrs[name];
if (param.optional && _.contains(DEFAULT_MISSING_PARAM_VALUES, val)) {
delete parsedAttrs[name];
} else {
switch (param.type) {
case 'node':
parsedAttrs[name] = (val || '').trim();
break;
case 'number':
parsedAttrs[name] = parseFloat(val, 10);
break;
case 'boolean':
parsedAttrs[name] = val === 'true' || val === true;
break;
default:
if (_.contains(DEFAULT_MISSING_PARAM_VALUES, val)) {
parsedAttrs[name] = null;
} else if (_.isString(val)) {
parsedAttrs[name] = val.trim();
}
}
}
}
return parsedAttrs;
},
getSourceNamesForAnalysisType: function (type) {
if (!SOURCE_NAMES_MAP[type]) {
var sourceNames = [];
var params = this.paramsForType(type);
for (var name in params) {
var param = params[name];
if (param.type === 'node') {
sourceNames.push(name);
}
}
SOURCE_NAMES_MAP[type] = sourceNames;
}
return SOURCE_NAMES_MAP[type];
},
getDefaultCartoCSSForType: function () {
return _.template([
"#layer['mapnik::geometry_type'=1] {",
' marker-width: <%= point.fill.size.fixed %>;',
' marker-fill: <%= point.fill.color.fixed %>;',
' marker-fill-opacity: <%= point.fill.color.opacity %>;',
' marker-line-color: <%= point.stroke.color.fixed %>;',
' marker-line-width: <%= point.stroke.size.fixed %>;',
' marker-line-opacity: <%= point.stroke.color.opacity %>;',
' marker-type: ellipse;',
' marker-allow-overlap: true;',
'}',
"#layer['mapnik::geometry_type'=2] {",
' line-color: <%= line.stroke.color.fixed %>;',
' line-width: <%= line.stroke.size.fixed %>;',
' line-opacity: 1;',
'}',
"#layer['mapnik::geometry_type'=3] {",
' polygon-fill: <%= polygon.fill.color.fixed %>;',
' polygon-opacity: <%= polygon.fill.color.opacity %>;',
' ::outline {',
' line-color: <%= polygon.stroke.color.fixed%>;',
' line-width: <%= polygon.stroke.size.fixed %>;',
' line-opacity: <%= polygon.stroke.color.opacity %>;',
' }',
'}'
].join('\n'))(DefaultCartography.simple);
},
isValidInputGeometryForType: function (simpleGeometryType, analysisType) {
var validGeometries = this.getValidInputGeometriesForType(analysisType);
return _.contains(validGeometries, simpleGeometryType) || _.contains(validGeometries, '*');
},
getValidInputGeometriesForType: function (analysisType) {
var params = this.paramsForType(analysisType);
var geometries = [];
for (var name in params) {
var param = params[name];
if (param.type === 'node') {
geometries = _.union(geometries, param.geometry);
}
}
return geometries;
}
};

View File

@@ -0,0 +1,5 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({
idAttribute: 'name'
});

View File

@@ -0,0 +1,67 @@
var _ = require('underscore');
var Backbone = require('backbone');
var CDB = require('internal-carto.js');
var queryTemplate = _.template('SELECT <%= column %> FROM (<%= sql %>) _table_sql GROUP BY <%= column %> ORDER BY <%= column %> ASC');
var MAX_ROW_COUNT = 100;
module.exports = Backbone.Model.extend({
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel param is required');
if (!opts.nodeDefModel) throw new Error('nodeDefModel param is required');
this._query = opts.nodeDefModel.querySchemaModel.get('query');
this._rowData = [];
this._SQL = new CDB.SQL({
user: opts.configModel.get('user_name'),
sql_api_template: opts.configModel.get('sql_api_template'),
api_key: opts.configModel.get('api_key')
});
this.on('change:column', this.fetch, this);
},
_onQueryDone: function (r) {
var resultCount = _.size(r.rows);
if (resultCount && resultCount < MAX_ROW_COUNT) {
this._rowData = _.pluck(r.rows, this.get('column'));
this.trigger('columnsFetched', this._rowData);
}
},
fetch: function () {
if (this.get('column') && this._query) {
this._SQL.execute(
queryTemplate({
sql: this._query,
column: this.get('column')
}),
null,
{
extra_params: ['page', 'rows_per_page'],
page: 0,
rows_per_page: 40,
success: this._onQueryDone.bind(this),
error: function () {
// TODO: what happens if fails?
}
}
);
}
},
getRows: function () {
var rowDataCount = _.size(this._rowData);
if (rowDataCount > 0) {
if (rowDataCount && rowDataCount < MAX_ROW_COUNT) {
return _.reject(this._rowData, _.isNull);
} else {
return false;
}
} else {
return false;
}
}
});

View File

@@ -0,0 +1,66 @@
var Backbone = require('backbone');
var _ = require('underscore');
/**
* 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({
defaults: {
base_url: '/CHANGEME' // expected to be set when model is created
},
urlVersion: function (modelName, method, defaultVersion) {
method = method || '';
var version = this.get(modelName + '_' + method + '_url_version');
return version || defaultVersion || 'v1';
},
/**
* @param {String} [version=v2] Version of API to use
* @return {String} the full sql api url, including the api endpoint, e.g. http://user.carto.com/api/v2/sql
*/
getSqlApiUrl: function (version) {
version = version || 'v2';
return this._getSqlApiBaseUrl() + '/api/' + version + '/sql';
},
/**
* @return {String} returns the base url to compose the final url, e.g. 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;
},
dataServiceEnabled: function (name) {
var dataServices = this.get('dataservices_enabled');
if (!dataServices || !_.size(dataServices) || !name) {
return false;
}
return !!dataServices[name];
},
dataLibraryEnabled: function () {
return this.get('data_library_enabled');
},
isHosted: function () {
return this.get('cartodb_com_hosted');
}
});

View File

@@ -0,0 +1,66 @@
/* global Image */
var Backbone = require('backbone');
var _ = require('underscore');
module.exports = Backbone.Model.extend({
getAttributes: function () {
return JSON.parse(JSON.stringify(this.attributes));
},
_generateClassName: function (urlTemplate) {
if (urlTemplate) {
return urlTemplate.replace(/\s+/g, '').replace(/[^a-zA-Z_0-9 ]/g, '').toLowerCase();
} else return '';
},
parse: function (data) {
var attrs = {};
_.extend(attrs, data.options, {
id: data.id,
className: this._generateClassName(data.options.urlTemplate),
type: 'Tiled',
order: data.order,
parent_id: data.parent_id
});
return attrs;
},
toJSON: function () {
var layerOptions = _.clone(_.omit(this.attributes, ['order', 'id']));
var json = {
kind: 'tiled',
options: layerOptions,
order: this.get('order'),
id: this.id
};
return json;
},
getValue: function () {
return this.get('val');
},
/**
* validateTemplateURL - Validates current urlTemplate of layer.
*
* @param {Object} callbacks with success and error functions defined to be called depending on validation outcome.
*/
validateTemplateURL: function (callbacks) {
var subdomains = ['a', 'b', 'c'];
var image = new Image();
image.onload = callbacks.success;
image.onerror = callbacks.error;
image.src = this.get('urlTemplate').replace(/\{s\}/g, function () {
return subdomains[Math.floor(Math.random() * 3)];
})
.replace(/\{x\}/g, '0')
.replace(/\{y\}/g, '0')
.replace(/\{z\}/g, '0');
}
});

View File

@@ -0,0 +1,54 @@
var Backbone = require('backbone');
var CustomBaselayerModel = require('./custom-baselayer-model');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'configModel',
'currentUserId'
];
module.exports = Backbone.Collection.extend({
model: function (d, opts) {
var self = opts.collection;
var m = new CustomBaselayerModel(d, {
parse: true,
collection: self
});
return m;
},
url: function () {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('user');
return baseUrl + '/api/' + version + '/users/' + this._currentUserId + '/layers';
},
initialize: function (models, opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
},
isCustomCategory: function () {
var custom = this.where(function (mdl) {
return ['NASA', 'TileJSON', 'WMS', 'Mapbox', 'Custom', undefined].some(function (category) {
return mdl.get('category') === category;
});
});
return custom;
},
getSelected: function () {
return this.findWhere({ selected: true });
},
hasCustomBaseLayer: function (className) {
if (!className) return;
var customBaseLayer = this.where({ className: className });
return customBaseLayer.length > 0;
}
});

View File

@@ -0,0 +1,60 @@
var BaseCollection = require('./data-observatory-base-collection');
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
// Interpolation boundary tag filters geometries to only show full coverage geometries
var FILTER_CLIPPED_GEOMS_IF_EXISTS = 'WHERE CASE WHEN EXISTS(SELECT 1 as cond FROM _data WHERE geom_tags ?\'boundary_type/tags.interpolation_boundary\' group by cond) THEN geom_tags ? \'boundary_type/tags.interpolation_boundary\' ELSE true END';
var ORDER_CLAUSE_GEOMS = 'ORDER BY geom_weight::numeric DESC;';
var COMMON_DATA_QUERY = 'bounds => (SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), numer_id => {{{ measurement }}}, number_geometries => (SELECT CDB_EstimateRowCount(\'{{{ query }}}\')::INTEGER)';
var BOUNDARIES_QUERY_PLAIN = 'WITH _data as (SELECT * FROM OBS_GetAvailableGeometries( ' + COMMON_DATA_QUERY + ' ) denoms WHERE valid_numer IS TRUE) SELECT *, rank() OVER (ORDER BY score DESC) FROM _data ' + FILTER_CLIPPED_GEOMS_IF_EXISTS + ' ' + ORDER_CLAUSE_GEOMS;
var BOUNDARIES_QUERY_NORMALIZE = 'WITH _data as (SELECT * FROM OBS_GetAvailableGeometries(' + COMMON_DATA_QUERY + ', denom_id => {{{ denom_id }}}) denoms WHERE valid_numer IS TRUE AND valid_denom IS TRUE) SELECT *, rank() OVER (ORDER BY score DESC) FROM _data ' + FILTER_CLIPPED_GEOMS_IF_EXISTS + ' ' + ORDER_CLAUSE_GEOMS;
var BOUNDARIES_QUERY_TIMESPAN = 'WITH _data as (SELECT * FROM OBS_GetAvailableGeometries(' + COMMON_DATA_QUERY + ', timespan => {{{ timespan }}}) denoms WHERE valid_numer IS TRUE AND valid_timespan = TRUE) SELECT *, rank() OVER (ORDER BY score DESC) FROM _data ' + FILTER_CLIPPED_GEOMS_IF_EXISTS + ' ' + ORDER_CLAUSE_GEOMS;
var BOUNDARIES_QUERY_NORMALIZE_TIMESPAN = 'WITH _data as (SELECT * FROM OBS_GetAvailableGeometries(' + COMMON_DATA_QUERY + ', denom_id => {{{ denom_id }}}, timespan => {{{ timespan }}}) denoms WHERE valid_numer IS TRUE AND valid_denom IS TRUE AND valid_timespan is TRUE) SELECT *, rank() OVER (ORDER BY score DESC) FROM _data ' + FILTER_CLIPPED_GEOMS_IF_EXISTS + ' ' + ORDER_CLAUSE_GEOMS;
module.exports = BaseCollection.extend({
model: function (attrs, opts) {
// label and val to custom list compatibility
var o = {};
o.val = attrs.geom_id;
o.label = attrs.geom_name;
o.rank = attrs.rank;
return new BaseModel(o);
},
getLabels: function () {
return this.pluck('label');
},
getValues: function () {
return this.pluck('val');
},
getPreSelectedBoundaryValue: function () {
var boundary = this.models.filter(
function (boundary) { return boundary.attributes.rank === 1; }
).shift();
return boundary && boundary.attributes && boundary.attributes.val;
},
buildQuery: function (options) {
if (options.denom_id != null && options.timespan != null) {
return BOUNDARIES_QUERY_NORMALIZE_TIMESPAN;
} else {
if (options.denom_id != null) {
return BOUNDARIES_QUERY_NORMALIZE;
} else if (options.timespan != null) {
return BOUNDARIES_QUERY_TIMESPAN;
} else {
return BOUNDARIES_QUERY_PLAIN;
}
}
}
});

View File

@@ -0,0 +1,74 @@
var _ = require('underscore');
var $ = require('jquery');
var cdb = require('internal-carto.js');
var errorParse = require('builder/helpers/error-parser');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'configModel',
'nodeDefModel'
];
var OBS_META_QUERY = 'SELECT OBS_GetMeta((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), {{{ metadata }}})';
var ColumnName = function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._querySchemaModel = this._nodeDefModel.querySchemaModel;
this.isFetching = false;
var configModel = options.configModel;
this.SQL = new cdb.SQL({
user: configModel.get('user_name'),
sql_api_template: configModel.get('sql_api_template'),
api_key: configModel.get('api_key')
});
};
ColumnName.prototype = {
buildQuery: function () {
return OBS_META_QUERY;
},
buildQueryOptions: function (options) {
return {
metadata: "'[" + JSON.stringify(options) + "]'",
query: this._querySchemaModel.get('query')
};
},
fetch: function (options) {
if (!options.numer_id) {
return;
}
this._success = options && options.success;
this._error = options && options.error;
var deferred = $.Deferred();
var queryOptions = this.buildQueryOptions(_.omit(options, 'sucess', 'error'));
var sqlQuery = this.buildQuery();
if (!this.isFetching) {
this.isFetching = true;
this.SQL.execute(sqlQuery, queryOptions, {
success: function (data) {
this.isFetching = false;
this._success && this._success(data);
deferred.resolve();
}.bind(this),
error: function (err) {
this.isFetching = false;
this._error && this._error(errorParse(err));
deferred.reject();
}.bind(this)
});
} else {
deferred.reject();
}
return deferred.promise();
}
};
module.exports = ColumnName;

View File

@@ -0,0 +1,105 @@
var Backbone = require('backbone');
var _ = require('underscore');
var $ = require('jquery');
var cdb = require('internal-carto.js');
var errorParse = require('builder/helpers/error-parser');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var CustomListCollection = require('builder/components/custom-list/custom-list-collection');
var REQUIRED_OPTS = [
'configModel',
'nodeDefModel'
];
module.exports = CustomListCollection.extend({
initialize: function (models, options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._querySchemaModel = this._nodeDefModel.querySchemaModel;
var configModel = options.configModel;
this.SQL = new cdb.SQL({
user: configModel.get('user_name'),
sql_api_template: configModel.get('sql_api_template'),
api_key: configModel.get('api_key')
});
this.stateModel = new Backbone.Model({
state: 'unfetched'
});
CustomListCollection.prototype.initialize.call(this, models, options);
},
_onSelectedChange: function (changedModel, isSelected) {
if (this.type === 'multiple') {
return;
}
if (isSelected) {
this.each(function (m) {
if (m.cid !== changedModel.cid) {
m.set({ selected: false }, { silent: true });
}
}, this);
}
},
buildQuery: function () {
throw new Error('subclasses of DataObservatoryBaseCollection must implement buildQuery');
},
fetch: function (options) {
this._success = options && options.success;
this._error = options && options.error;
var defaults = {
filters: null,
measurement: null,
normalize: null,
timespan: null,
query: this._querySchemaModel.get('query')
};
this.deferred = $.Deferred();
var queryOptions = _.defaults(options, defaults);
var sqlQuery = this.buildQuery(queryOptions);
this.stateModel.set('state', 'fetching');
this.SQL.execute(sqlQuery, queryOptions, {
success: function (data) {
this._onFetchSuccess(data);
this.stateModel.set('state', 'fetched');
this.deferred.resolve();
this._success && this._success();
}.bind(this),
error: function (err) {
this.stateModel.set('state', 'error');
this.deferred.reject();
this._error && this._error(errorParse(err));
}.bind(this)
});
return this.deferred.promise();
},
_onFetchSuccess: function (data) {
var models = data.rows;
this.reset(models);
},
isFetching: function () {
return this.getState() === 'fetching';
},
getState: function () {
return this.stateModel.get('state');
},
getItem: function (value) {
return this.findWhere({ val: value });
},
isAsync: function () {
return true;
}
});

View File

@@ -0,0 +1,63 @@
var _ = require('underscore');
var BaseCollection = require('./data-observatory-base-collection');
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
var FILTERS_QUERY_WITH_REGION = "SELECT count(*) num_measurements, tag.key subsection_id, tag.value subsection_name FROM OBS_GetAvailableNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), {{{ region }}}) numers, Jsonb_Each(numers.numer_tags) tag WHERE tag.key like 'subsection%' GROUP BY tag.key, tag.value";
module.exports = BaseCollection.extend({
initialize: function (models, options) {
this.type = 'multiple';
BaseCollection.prototype.initialize.call(this, models, options);
},
model: function (attrs, opts) {
// label and val to custom list compatibility
var o = {};
o.val = attrs.subsection_id;
o.label = attrs.subsection_name;
o.items = attrs.num_measurements;
return new BaseModel(o);
},
setSelected: function (value) {
var silentTrue = { silent: true };
if (_.isArray(value)) {
this.each(function (mdl) {
if (_.contains(value, mdl.getValue())) {
mdl.set({
selected: true
}, silentTrue);
} else {
mdl.set({
selected: false
}, silentTrue);
}
});
} else {
this.each(function (mdl) {
if (mdl.getValue() === value) {
mdl.set({
selected: true
}, silentTrue);
} else {
mdl.set({
selected: false
}, silentTrue);
}
});
}
},
getSelected: function () {
return this.filter(function (mdl) {
return mdl.get('selected') === true;
});
},
buildQuery: function (options) {
return FILTERS_QUERY_WITH_REGION;
}
});

View File

@@ -0,0 +1,111 @@
var _ = require('underscore');
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
var $ = require('jquery');
var cdb = require('internal-carto.js');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'configModel',
'nodeDefModel'
];
var MEASUREMENT_ATTRIBUTES = ['aggregate', 'type', 'label', 'val', 'description', 'filter', 'license'];
var MEASUREMENT_BY_ID = 'SELECT numer_id, numer_name, numer_description, numer_type, numer_aggregate, numer_tags FROM _OBS_GetNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), section_tags => {{{ area }}}, ids => {{{ measurement }}})';
module.exports = BaseModel.extend({
initialize: function (attrs, options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._querySchemaModel = this._nodeDefModel.querySchemaModel;
var configModel = options.configModel;
this.SQL = new cdb.SQL({
user: configModel.get('user_name'),
sql_api_template: configModel.get('sql_api_template'),
api_key: configModel.get('api_key')
});
BaseModel.prototype.initialize.call(this, attrs, options);
},
fetch: function (options) {
var defaults = {
area: null,
measurement: null,
query: this._querySchemaModel.get('query')
};
this.deferred = $.Deferred();
var queryOptions = _.defaults(options, defaults);
this.set('state', 'fetching');
this.SQL.execute(MEASUREMENT_BY_ID, queryOptions, {
success: function (data) {
this._onFetchSuccess(data);
this.set('state', 'fetched');
this.deferred.resolve();
}.bind(this),
error: function () {
this.set('state', 'error');
this.deferred.reject();
}.bind(this)
});
return this.deferred.promise();
},
isFetching: function () {
return this.getState() === 'fetching';
},
getState: function () {
return this.get('state');
},
_onFetchSuccess: function (data) {
var model = _.first(data.rows);
if (!model) {
this._clear();
return;
}
var o = {};
// label and val to custom list compatibility
o.val = model.numer_id;
o.label = model.numer_name;
o.description = model.numer_description;
o.type = model.numer_type;
o.aggregate = model.numer_aggregate;
// a measurement can belong to more than one category (filter)
o.filter = [];
var tags = model.numer_tags;
if (!_.isObject(tags)) {
tags = JSON.parse(tags);
}
for (var key in tags) {
if (/^subsection/.test(key)) {
o.filter.push({
id: key,
name: tags[key]
});
}
if (/^license/.test(key)) {
o.license = tags[key];
}
}
this.set(o);
},
_clear: function (options) {
var attrs = MEASUREMENT_ATTRIBUTES.reduce(function (memo, attr) {
memo[attr] = null;
return memo;
}, {});
this.set(attrs, {silent: true});
}
});

View File

@@ -0,0 +1,72 @@
var _ = require('underscore');
var BaseCollection = require('./data-observatory-base-collection');
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
var MEASUREMENTS_QUERY_WITH_REGION = 'SELECT numer_id, numer_name, numer_description, numer_type, numer_aggregate, numer_tags FROM _OBS_GetNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), section_tags => {{{ region }}}) ORDER BY numer_name ASC';
var MEASUREMENTS_QUERY_WITH_FILTERS = 'SELECT numer_id, numer_name, numer_description, numer_type, numer_aggregate, numer_tags FROM _OBS_GetNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), section_tags => {{{ region }}}, subsection_tags => {{{ filters }}})';
var MEASUREMENTS_QUERY_SEARCH = "SELECT numer_id, numer_name, numer_description, numer_type, numer_aggregate, numer_tags FROM _OBS_GetNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), section_tags => {{{ region }}}, name => '{{{ search }}}')";
var MEASUREMENTS_QUERY_SEARCH_AND_FILTERS = "SELECT numer_id, numer_name, numer_description, numer_type, numer_aggregate, numer_tags FROM _OBS_GetNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), section_tags => {{{ region }}} , name => '{{{ search }}}', subsection_tags => {{{ filters }}})";
module.exports = BaseCollection.extend({
model: function (attrs, opts) {
return new BaseModel(attrs, opts);
},
_onFetchSuccess: function (data) {
var models = data.rows;
models = _.map(models, function (model) {
var o = {};
// label and val to custom list compatibility
o.val = model.numer_id;
o.label = model.numer_name;
o.description = model.numer_description;
o.type = model.numer_type;
o.aggregate = model.numer_aggregate;
// a measurement can belong to more than one category (filter)
o.filter = [];
var tags = model.numer_tags;
if (!_.isObject(tags)) {
tags = JSON.parse(tags);
}
for (var key in tags) {
if (/^subsection/.test(key)) {
o.filter.push({
id: key,
name: tags[key]
});
}
if (/^license/.test(key)) {
o.license = tags[key];
}
}
return o;
}).filter(function (model) {
return model.filter.length > 0;
});
this.reset(models);
},
buildQuery: function (options) {
if (options && options.search && options.filters) {
return MEASUREMENTS_QUERY_SEARCH_AND_FILTERS;
} else if (options && options.filters) {
return MEASUREMENTS_QUERY_WITH_FILTERS;
} else if (options && options.search) {
return MEASUREMENTS_QUERY_SEARCH;
} else {
return MEASUREMENTS_QUERY_WITH_REGION;
}
},
search: function () {
return this;
}
});

View File

@@ -0,0 +1,19 @@
var BaseCollection = require('./data-observatory-base-collection');
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
var NORMALIZE_QUERY_WITH_MEASUREMENT = 'SELECT * FROM OBS_GetAvailableDenominators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), NULL, {{{ measurement }}}) denoms WHERE valid_numer IS TRUE';
module.exports = BaseCollection.extend({
model: function (attrs, opts) {
// label and val to custom list compatibility
var o = {};
o.val = attrs.denom_id;
o.label = attrs.denom_name;
return new BaseModel(o);
},
buildQuery: function (options) {
return NORMALIZE_QUERY_WITH_MEASUREMENT;
}
});

View File

@@ -0,0 +1,35 @@
var BaseCollection = require('./data-observatory-base-collection');
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
var REGIONS_QUERY = "SELECT count(*) num_measurements, tag.key region_id, tag.value region_name FROM (SELECT * FROM OBS_GetAvailableNumerators() WHERE jsonb_pretty(numer_tags) LIKE '%subsection/%') numers, Jsonb_Each(numers.numer_tags) tag WHERE tag.key like 'section%' GROUP BY tag.key, tag.value ORDER BY region_name";
module.exports = BaseCollection.extend({
buildQuery: function () {
return REGIONS_QUERY;
},
model: function (attrs, opts) {
// label and val to custom list compatibility
var o = {};
o.val = attrs.region_id;
o.label = attrs.region_name.replace(/["]+/g, '');
o.renderOptions = {
measurements: attrs.num_measurements
};
return new BaseModel(o);
},
fetch: function (options) {
this.stateModel.set('state', 'fetching');
// check if the geometry type is available, if not wait until it's finished
if (this._nodeDefModel.queryGeometryModel.isFetched()) {
BaseCollection.prototype.fetch.call(this, options);
} else {
this._nodeDefModel.queryGeometryModel.once('change:status', function () {
BaseCollection.prototype.fetch.call(this, options);
}, this);
}
}
});

View File

@@ -0,0 +1,32 @@
var BaseCollection = require('./data-observatory-base-collection');
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
var TIMESPAN_QUERY_WITH_NORMALIZE = 'SELECT * FROM OBS_GetAvailableTimespans((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), NULL, {{{ measurement }}}, {{{ denom_id }}}) denoms WHERE valid_numer IS TRUE AND (valid_denom IS TRUE OR {{{ denom_id }}} IS NULL) ORDER BY timespan_name DESC';
var TIMESPAN_QUERY_WITHOUT_NORMALIZE = 'SELECT * FROM OBS_GetAvailableTimespans((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), NULL, {{{ measurement }}}, NULL) denoms WHERE valid_numer IS TRUE ORDER BY timespan_name DESC';
module.exports = BaseCollection.extend({
model: function (attrs, opts) {
// label and val to custom list compatibility
var o = {};
o.val = attrs.timespan_id;
o.label = attrs.timespan_name;
return new BaseModel(o);
},
buildQuery: function (options) {
if (options.denom_id != null) {
return TIMESPAN_QUERY_WITH_NORMALIZE;
} else {
return TIMESPAN_QUERY_WITHOUT_NORMALIZE;
}
},
selectFirstAsDefault: function () {
var first = this.first();
if (first) {
this.setSelected(first.getValue());
}
}
});

View File

@@ -0,0 +1,56 @@
{
"simple": {
"point": {
"fill": {
"size": {
"fixed": 7
},
"color": {
"fixed": "#EE4D5A",
"opacity": 0.9
}
},
"stroke": {
"size": {
"fixed": 1
},
"color": {
"fixed": "#FFFFFF",
"opacity": 1
}
}
},
"line": {
"fill": {},
"stroke": {
"size": {
"fixed": 1.5
},
"color": {
"fixed": "#4CC8A3",
"opacity": 1
}
}
},
"polygon": {
"fill": {
"color": {
"fixed": "#826DBA",
"opacity": 0.9
}
},
"stroke": {
"size": {
"fixed": 1
},
"color": {
"fixed": "#FFFFFF",
"opacity": 0.5
}
}
}
}
}

View File

@@ -0,0 +1,48 @@
{
"fill": {
"size": {
"fixed": 7
},
"color": {
"fixed": "#EE4D5A",
"opacity": 0.9
}
},
"stroke": {
"size": {
"fixed": 0
},
"color": {
"fixed": "#FFFFFF",
"opacity": 1
}
},
"blending": "none",
"aggregation": {},
"labels": {
"enabled": false,
"attribute": null,
"font": "Source Han Sans CN Regular",
"fill": {
"size": {
"fixed": 14
},
"color": {
"fixed": "#000000",
"opacity": 1
}
},
"halo": {
"size": {
"fixed": 2
},
"color": {
"fixed": "#FFFFFF",
"opacity": 1
}
},
"offset": -10,
"overlap": true,
"placement": "point"
}
}

View File

@@ -0,0 +1,37 @@
var BackgroundPollingModel = require('./background-importer/background-polling-model');
var TableModel = require('./table-model');
/**
* Background polling model for the editor context.
*/
module.exports = BackgroundPollingModel.extend({
initialize: function (attrs, opts) {
BackgroundPollingModel.prototype.initialize.apply(this, arguments);
if (!opts.userActions) { throw new Error('userActions is required'); }
this._userActions = opts.userActions;
},
_onImportsStateChange: function (importsModel) {
if (importsModel.hasCompleted()) {
var tableName = importsModel.importedVis().get('table').name;
var tableModel = new TableModel({
name: tableName
}, {
configModel: this._configModel
});
// Provide table geometry in order to set properly styles when
// layer is added to the current map
var onTableFetched = function () {
this._userActions.createLayerFromTable(tableModel);
this.trigger('importCompleted', importsModel, this);
}.bind(this);
tableModel.fetch().always(onTableFetched);
}
}
});

View File

@@ -0,0 +1,17 @@
var Backbone = require('backbone');
/**
* Model for general editor configuration.
*/
module.exports = Backbone.Model.extend({
defaults: {
edition: false,
disabled: false
},
isEditing: function () {
return !!this.get('edition');
},
isDisabled: function () {
return !!this.get('disabled');
}
});

View File

@@ -0,0 +1,66 @@
var Backbone = require('backbone');
/**
* Model that represents a visualization export (v3)
*/
module.exports = Backbone.Model.extend({
initialize: function (attrs, opts) {
if (!attrs.visualization_id) throw new Error('visualization_id is required');
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
},
urlRoot: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v3/visualization_exports';
},
requestExport: function () {
this.save(null, {
success: this._requestExportSuccessHandler.bind(this)
});
// TODO add metrics for private and public exports
},
cancelExport: function () {
this._interrupt();
},
_requestExportSuccessHandler: function () {
this._pollPID = setInterval(function () {
this.fetch({
success: this._checkState.bind(this),
error: this._errorHandler.bind(this)
});
}.bind(this), 2000);
},
_checkState: function () {
if (this.get('state') === 'complete') {
this._finishExport();
} else if (this.get('state') === 'failure') {
this._errorHandler();
}
},
_finishExport: function () {
clearInterval(this._pollPID);
},
_errorHandler: function () {
this._interrupt();
throw new Error('There is a problem with your export. Please try again.');
},
_interrupt: function () {
clearInterval(this._pollPID);
this.set('state', 'failure');
}
});

View File

@@ -0,0 +1,232 @@
var _ = require('underscore');
var Backbone = require('backbone');
var QueryRowModel = require('./query-row-model');
var TYPES_POINT = 'point';
var TYPES_LINE = 'line';
var TYPES_POLYGON = 'polygon';
var GEOJSON_TO_FEATURE_TYPE = {
Point: TYPES_POINT,
MultiPoint: TYPES_POINT,
LineString: TYPES_LINE,
MultiLineString: TYPES_LINE,
Polygon: TYPES_POLYGON,
MultiPolygon: TYPES_POLYGON
};
var convertGeoJSONTypeToFeatureType = function (the_geom) {
var geoJSON = null;
try {
geoJSON = JSON.parse(the_geom);
} catch (err) {
// if the geom is not a valid json value
}
var geometryType = geoJSON && geoJSON.type;
var featureType = GEOJSON_TO_FEATURE_TYPE[geometryType];
if (!featureType) {
throw new Error("unsupported geometry type: '" + geometryType + "'");
}
return featureType;
};
var BLACKLISTED_COLUMNS = ['cartodb_id', 'created_at', 'the_geom_webmercator', 'updated_at'];
var FeatureDefinitionModel = Backbone.Model.extend({
initialize: function (attrs, options) {
if (!options.configModel) throw new Error('configModel is required');
if (!options.layerDefinitionModel) throw new Error('layerDefinitionModel is required');
if (!options.userModel) throw new Error('userModel is required');
this._configModel = options.configModel;
this._layerDefinitionModel = options.layerDefinitionModel;
this._userModel = options.userModel;
this._featureType = options.featureType;
this._firstNode = this._getAnalysisDefinitionNodeModel();
this._changesHistory = [];
if (this._firstNode.isSourceType()) {
this._tableNodeModel = this._firstNode.getTableModel();
}
this._onChange = _.debounce(this._onChange.bind(this), 500);
if (this.isNew()) {
// Bind change event in order to "store" all changed columns after creation
this._bindChangeEvent();
}
},
isNew: function () {
return !this.has('cartodb_id');
},
isPoint: function () {
return this._isType(TYPES_POINT);
},
isPolygon: function () {
return this._isType(TYPES_POLYGON);
},
isLine: function () {
return this._isType(TYPES_LINE);
},
_isType: function (type) {
return this._getType() === type;
},
isEditable: function () {
var analysisDefinitionModel = this._getAnalysisDefinitionNodeModel();
return !analysisDefinitionModel.isReadOnly();
},
_getPermissionModel: function () {
return this._tableNodeModel && this._tableNodeModel.getPermissionModel();
},
isReadOnly: function () {
return this._tableNodeModel && this._tableNodeModel.isReadOnly(this._userModel);
},
hasAnalyses: function () {
return this._layerDefinitionModel.hasAnalyses();
},
isCustomQueryApplied: function () {
var analysisDefinitionModel = this._getAnalysisDefinitionNodeModel();
return analysisDefinitionModel.isCustomQueryApplied();
},
_getAnalysisDefinitionNodeModel: function () {
return this._layerDefinitionModel.getAnalysisDefinitionNodeModel();
},
isEqual: function (featureDefinition) {
return this.get('cartodb_id') === featureDefinition.get('cartodb_id') &&
this.getLayerId() === featureDefinition.getLayerId();
},
getLayerId: function () {
return this._layerDefinitionModel.id;
},
getLayerDefinition: function () {
return this._layerDefinitionModel;
},
_cleanChangesHistory: function () {
this._changesHistory = [];
},
_bindChangeEvent: function () {
this.on('change', this._onChange, this);
},
_unbindChangeEvent: function () {
this.off('change', this._onChange, this);
},
_onChange: function () {
_.each(this.changed, function (value, key) {
if (!_.contains(this._changesHistory, key)) {
this._changesHistory.push(key);
}
}, this);
},
hasBeenChangedAfterLastSaved: function (key) {
return _.contains(this._changesHistory, key);
},
getFeatureType: function () {
if (this.isPoint()) {
return 'point';
}
if (this.isLine()) {
return 'line';
}
if (this.isPolygon()) {
return 'polygon';
}
},
fetch: function (options) {
options = options || {};
this._unbindChangeEvent();
this._getQueryRowModel().fetch({
success: function (data) {
this.set(data.toJSON());
// Bind change event in order to "store" all changed columns after starting edition
this._bindChangeEvent();
options.success && options.success();
}.bind(this)
});
},
save: function (options) {
options = options || {};
var columns = this._layerDefinitionModel.getColumnNamesFromSchema();
var attrs = _.pick(this.toJSON(), _.difference(columns, BLACKLISTED_COLUMNS));
this._getQueryRowModel().save(attrs, {
success: function (queryRowModel) {
if (this.isNew()) {
this.set('cartodb_id', queryRowModel.get('cartodb_id'));
}
this.trigger('save', this, options);
options.success && options.success();
this._cleanChangesHistory();
}.bind(this),
error: function () {
options.error && options.error();
}
});
},
destroy: function (options) {
options = options || {};
this._getQueryRowModel().destroy({
success: function () {
this.trigger('remove', this, options);
options.success && options.success();
}.bind(this),
error: function () {
options.error && options.error();
}
});
},
_getQueryRowModel: function () {
var attrs = {};
if (this.has('cartodb_id')) {
attrs.cartodb_id = this.get('cartodb_id');
}
this._queryRowModel = this._queryRowModel || new QueryRowModel(attrs, {
tableName: this._layerDefinitionModel.getTableName(),
configModel: this._configModel,
permissionModel: this._getPermissionModel(),
userModel: this._userModel
});
return this._queryRowModel;
},
_getType: function () {
var the_geom = this.get('the_geom');
var type = the_geom && convertGeoJSONTypeToFeatureType(the_geom) || this._featureType;
return type;
}
});
module.exports = FeatureDefinitionModel;

View File

@@ -0,0 +1,16 @@
var MAP = {
'st_multipolygon': 'polygon',
'st_polygon': 'polygon',
'st_multilinestring': 'line',
'st_linestring': 'line',
'st_multipoint': 'point',
'st_point': 'point'
};
/**
* @param {String} val e.g. 'ST_MultiPoint'
* @return {String} e.g. 'point'
*/
module.exports = function (val) {
return MAP[val.toLowerCase()];
};

View File

@@ -0,0 +1,78 @@
var Backbone = require('backbone');
var _ = require('underscore');
var syncAbort = require('./backbone/sync-abort');
var UserModel = require('./user-model');
var GroupModel = require('./group-model');
/**
* A collection of Grantable objects.
*/
module.exports = Backbone.Collection.extend({
model: function (attrs, options) {
var type = attrs.type;
var configModel = attrs.configModel;
options = _.extend(options, {
configModel: configModel
});
if (attrs.model) {
attrs = _.extend({}, attrs, attrs.model);
}
if (type === 'user') {
return new UserModel(_.omit(attrs, 'configModel'), options);
} else if (type === 'group') {
return new GroupModel(_.omit(attrs, 'configModel'), options);
}
},
sync: syncAbort,
url: function (method) {
var baseUrl = this.configModel.get('base_url');
var version = this.configModel.urlVersion('organizationGrantables', method);
return baseUrl + '/api/' + version + '/organization/' + this.organization.id + '/grantables';
},
initialize: function (users, opts) {
if (!opts.organization) throw new Error('Organization is required');
if (!opts.currentUserId) throw new Error('currentUserId is required');
if (!opts.configModel) throw new Error('configModel is required');
this.configModel = opts.configModel;
this.organization = opts.organization;
this.currentUserId = opts.currentUserId;
},
parse: function (response) {
this.trigger('fetched', this);
this.total_entries = response.total_entries;
return _.reduce(response.grantables, function (memo, m) {
if (m.id === this.currentUserId) {
this.total_entries--;
} else {
m.organization = this.organization;
m.configModel = this.configModel;
memo.push(m);
}
return memo;
}, [], this);
},
fetch: function (opts) {
opts = opts || {};
this.trigger('fetching', this);
opts.error = function (model, response) {
this.trigger('error', this);
}.bind(this);
return Backbone.Collection.prototype.fetch.call(this, opts);
},
totalCount: function () {
return this.total_entries;
}
});

View File

@@ -0,0 +1,29 @@
var Backbone = require('backbone');
var UsersGroup = require('./users-group-collection');
/**
* Model representing a group.
* Expected to be used in the context of a groups collection (e.g. OrganizationGroups),
* which defines its API endpoint path.
*/
module.exports = Backbone.Model.extend({
defaults: {
display_name: ''
},
initialize: function (attrs, opts) {
this.parse(attrs || {}, opts); // handle given attrs in the same way as for .fetch()
},
parse: function (attrs, opts) {
this.users = new UsersGroup(attrs.users, {
group: this,
configModel: opts.configModel,
organization: opts.organization
});
return attrs;
}
});

View File

@@ -0,0 +1,73 @@
var InfowindowDefinitionModel = require('./infowindow-definition-model');
module.exports = InfowindowDefinitionModel.extend({
defaults: {
template_name: '',
latlng: [0, 0],
offset: [28, 0], // offset of the tip calculated from the bottom left corner
maxHeight: 180, // max height of the content, not the whole infowindow
autoPan: true,
template: '',
content: '',
visibility: false,
alternative_names: { },
fields: null, // contains the fields displayed in the infowindow,
width: 226,
headerColor: {
color: {
fixed: '#35AAE5',
opacity: 1
}
}
},
TEMPLATES: {
infowindow_light: require('builder/mustache-templates/infowindows/infowindow_light.jst.mustache'),
infowindow_dark: require('builder/mustache-templates/infowindows/infowindow_dark.jst.mustache'),
infowindow_color: require('builder/mustache-templates/infowindows/infowindow_color.jst.mustache'),
infowindow_header_with_image: require('builder/mustache-templates/infowindows/infowindow_header_with_image.jst.mustache'),
infowindow_header_with_video: require('builder/mustache-templates/infowindows/infowindow_header_with_video.jst.mustache'),
infowindow_header_with_url: require('builder/mustache-templates/infowindows/infowindow_header_with_url.jst.mustache'),
custom_infowindow_light: require('builder/mustache-templates/infowindows_custom/infowindow_light.jst.mustache'),
custom_infowindow_dark: require('builder/mustache-templates/infowindows_custom/infowindow_dark.jst.mustache'),
custom_infowindow_color: require('builder/mustache-templates/infowindows_custom/infowindow_color.jst.mustache'),
custom_infowindow_header_with_image: require('builder/mustache-templates/infowindows_custom/infowindow_header_with_image.jst.mustache')
},
_transformTemplate: function (template) {
var fixed = this.get('headerColor').color.fixed;
return template.replace('background: #35AAE5', 'background: ' + fixed);
},
setTemplate: function (templateName) {
var template = (typeof (this.TEMPLATES[templateName]) === 'undefined') ? '' : this.TEMPLATES[templateName];
if (templateName === 'infowindow_color') {
template = this._transformTemplate(template);
}
var attrs = {
'template_name': templateName,
'template': template,
'template_type': 'mustache'
};
if (templateName === '') {
attrs.fields = [];
}
this.set(attrs);
},
isEmptyTemplate: function () {
var isEmpty = this.fieldCount() === 0;
return this.hasTemplate() && isEmpty;
},
setDefault: function () {
this.setTemplate(this.defaults.template_name);
}
});

View File

@@ -0,0 +1,184 @@
var Backbone = require('backbone');
var _ = require('underscore');
var $ = require('jquery');
module.exports = Backbone.Model.extend({
SYSTEM_COLUMNS: ['the_geom', 'the_geom_webmercator', 'created_at', 'updated_at', 'cartodb_id', 'cartodb_georef_status'],
TEMPLATES: {},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
},
clearFields: function () {
this.set({ 'fields': [] });
},
saveFields: function (where) {
where = where || 'old_fields';
this.set(where, _.clone(this.get('fields')));
},
fieldCount: function () {
var fields = this.get('fields');
if (!fields) return 0;
return fields.length;
},
restoreFields: function (whiteList, from) {
from = from || 'old_fields';
var fields = this.get(from);
if (whiteList) {
fields = fields.filter(function (f) {
return _.contains(whiteList, f.name);
});
}
if (fields && fields.length) {
this._setFields(fields);
}
this.unset(from);
},
containsField: function (fieldName) {
var fields = this.get('fields') || [];
return _.contains(_(fields).pluck('name'), fieldName);
},
getFieldProperty: function (fieldName, k) {
if (this.containsField(fieldName)) {
var fields = this.get('fields') || [];
var idx = _.indexOf(_(fields).pluck('name'), fieldName);
return fields[idx][k];
}
return null;
},
setFieldProperty: function (fieldName, k, v) {
if (this.containsField(fieldName)) {
var fields = this._cloneFields() || [];
var idx = _.indexOf(_(fields).pluck('name'), fieldName);
fields[idx][k] = v;
this._setFields(fields);
}
return this;
},
_cloneFields: function () {
return _(this.get('fields')).map(function (v) {
return _.clone(v);
});
},
_setFields: function (f) {
f.sort(function (a, b) { return a.position - b.position; });
this.set({ 'fields': f });
},
sortFields: function () {
this.get('fields').sort(function (a, b) { return a.position - b.position; });
},
_addField: function (fieldName, at) {
var dfd = $.Deferred();
if (!this.containsField(fieldName)) {
var fields = this.get('fields');
if (fields) {
at = at === undefined ? fields.length : at;
fields.push({ name: fieldName, title: true, position: at });
} else {
at = at === undefined ? 0 : at;
this.set('fields', [{ name: fieldName, title: true, position: at }], { silent: true });
}
}
dfd.resolve();
return dfd.promise();
},
addField: function (fieldName, at) {
var self = this;
$.when(this._addField(fieldName, at)).then(function () {
self.sortFields();
self.trigger('add:fields');
self.trigger('change:fields', self, self.get('fields'));
self.trigger('change', self);
});
return this;
},
removeField: function (fieldName) {
if (this.containsField(fieldName)) {
var fields = this._cloneFields() || [];
var idx = _.indexOf(_(fields).pluck('name'), fieldName);
if (idx >= 0) {
fields.splice(idx, 1);
}
this._setFields(fields);
this.trigger('remove:fields');
}
return this;
},
getAlternativeName: function (fieldName) {
return this.get('alternative_names') && this.get('alternative_names')[fieldName];
},
setAlternativeName: function (fieldName, alternativeName) {
var alternativeNames = _.clone(this.get('alternative_names') || {});
alternativeNames[fieldName] = alternativeName;
this.set({ 'alternative_names': alternativeNames });
this.trigger('change:alternative_names');
},
getFieldPos: function (fieldName) {
var p = this.getFieldProperty(fieldName, 'position');
if (p === undefined) {
return Number.MAX_VALUE;
}
return p;
},
unsetTemplate: function () {
this.setTemplate('');
},
setTemplate: function (templateName) {
var template = (typeof (this.TEMPLATES[templateName]) === 'undefined') ? '' : this.TEMPLATES[templateName];
var attrs = {
'template_name': templateName,
'template': template,
'template_type': 'mustache'
};
if (templateName === '') {
attrs.fields = [];
}
this.set(attrs);
},
getTemplate: function () {
var template_name = this.get('template_name');
if (template_name !== '') {
return this.TEMPLATES['custom_' + template_name];
} else {
return this.get('template');
}
},
hasTemplate: function () {
return !!this.get('template_name') && this.TEMPLATES[this.get('template_name')];
},
isCustomTemplate: function () {
return this.get('template_name') === '' && this.get('template') !== '';
}
});

View File

@@ -0,0 +1,24 @@
var InfowindowDefinitionModel = require('./infowindow-definition-model');
module.exports = InfowindowDefinitionModel.extend({
defaults: {
vertical_offset: 0,
horizontal_offset: 0,
position: 'top|center',
template_name: '',
template: ''
},
TEMPLATES: {
tooltip_dark: require('builder/mustache-templates/tooltips/tooltip_dark.jst.mustache'),
tooltip_light: require('builder/mustache-templates/tooltips/tooltip_light.jst.mustache'),
custom_tooltip_dark: require('builder/mustache-templates/tooltips_custom/tooltip_dark.jst.mustache'),
custom_tooltip_light: require('builder/mustache-templates/tooltips_custom/tooltip_light.jst.mustache')
},
setDefault: function () {
this.setTemplate(this.defaults.template_name);
}
});

View File

@@ -0,0 +1,29 @@
module.exports = {
COLORS: [
'#11A579',
'#E83f74',
'#9F5DC8',
'#EF8205',
'#29B3B2',
'#E54F3B',
'#59BB12',
'#EF57BA',
'#E2AF00',
'#12AADE'
],
/**
* Returns a color given a letter
* @param {String} Letter. eg: 'a', 'b', 'c', etc.
* @return {String} Hex color code: eg: '#7F3C8D'
*/
getColorForLetter: function (letter) {
if (!letter) {
return this.COLORS[0];
}
var letterNumber = letter.charCodeAt(0) - 97;
var colorIndex = ((letterNumber / this.COLORS.length % 1) * 10).toFixed();
return this.COLORS[colorIndex] || this.COLORS[0];
}
};

View File

@@ -0,0 +1,86 @@
var Backbone = require('backbone');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var STATES = require('./query-base-status');
var CONTEXTS = {
map: 'map',
table: 'table'
};
var REQUIRED_OPTS = [
'querySchemaModel',
'queryGeometryModel',
'queryRowsCollection'
];
var LayerContentModel = Backbone.Model.extend({
defaults: {
context: CONTEXTS.map, // map or table
// This variable `state` is only used for the trigger change:state event.
// It lives in the model so, as long as the model is recreated with the view every time,
// this variable is not shared between views and can NOT use it to get the real state.
state: STATES.initial
},
initialize: function (attrs, opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initBinds();
},
_initBinds: function () {
this.listenTo(this._querySchemaModel, 'change:status', this._setState);
this.listenTo(this._queryGeometryModel, 'change:status', this._setState);
this.listenTo(this._queryRowsCollection.statusModel, 'change:status', this._setState);
},
_setState: function () {
this.set('state', this._getState());
},
_getState: function () {
if (this.isErrored()) return STATES.errored;
if (this.isFetched()) return STATES.fetched;
if (this.isFetching()) return STATES.fetching;
if (this.isInitial()) return STATES.initial;
return STATES.unavailable;
},
isErrored: function () {
return this._querySchemaModel.hasRepeatedErrors() ||
this._queryGeometryModel.hasRepeatedErrors() ||
this._queryRowsCollection.hasRepeatedErrors();
},
isFetching: function () {
return this._querySchemaModel.isFetching() ||
this._queryGeometryModel.isFetching() ||
this._queryRowsCollection.isFetching();
},
isFetched: function () {
return this._querySchemaModel.isFetched() &&
this._queryGeometryModel.isFetched() &&
this._queryRowsCollection.isFetched();
},
isInFinalStatus: function () {
return this._querySchemaModel.isInFinalStatus() &&
this._queryGeometryModel.isInFinalStatus() &&
this._queryRowsCollection.isInFinalStatus();
},
isInitial: function () {
return this.get('state') === STATES.initial;
},
isDone: function () {
return this.isFetched() || this.isErrored();
}
});
LayerContentModel.CONTEXTS = CONTEXTS;
LayerContentModel.STATES = STATES;
module.exports = LayerContentModel;

View File

@@ -0,0 +1,489 @@
var Backbone = require('backbone');
var _ = require('underscore');
var syncAbort = require('./backbone/sync-abort');
var StyleDefinitionModel = require('builder/editor/style/style-definition-model');
var StyleCartoCSSModel = require('builder/editor/style/style-cartocss-model');
var DataSQLModel = require('builder/editor/layers/layer-content-views/data/data-sql-model');
var layerTypesAndKinds = require('./layer-types-and-kinds');
var InfowindowModel = require('./infowindow-click-model');
var TooltipModel = require('./infowindow-hover-model');
var TableNameUtils = require('builder/helpers/table-name-utils');
var layerColors = require('./layer-colors');
// from_layer_id and from_letter are not attributes for the model, but are sent to the layer creation
// endpoint when creating a layer from an existing analysis node (see user-actions)
var ATTR_NAMES = ['id', 'order', 'infowindow', 'tooltip', 'error', 'from_layer_id', 'from_letter'];
/**
* Model to edit a layer definition.
* Should always exist as part of a LayerDefinitionsCollection, so its URL is given from there.
*/
module.exports = Backbone.Model.extend({
/**
* @override {Backbone.prototype.sync} abort ongoing request if there is any
*/
sync: syncAbort,
parse: function (response, opts) {
response.options = response.options || {};
// Flatten the attrs, to avoid having this.get('options').foobar internally
var attrs = _
.defaults(
_.pick(response, ATTR_NAMES),
_.omit(response.options, ['query', 'tile_style'])
);
// Only use type on the frontend, it will be mapped back when the model is serialized (see .toJSON)
attrs.type = attrs.type || layerTypesAndKinds.getType(response.kind);
// Map API endpoint attrs to the new names used client-side (cartodb.js in particular)
if (response.options.tile_style) {
attrs.cartocss = response.options.tile_style;
}
if (response.options.query) {
attrs.sql = response.options.query;
}
if (response.infowindow) {
if (!this.infowindowModel) {
this.infowindowModel = new InfowindowModel(response.infowindow, {
configModel: opts.configModel || this._configModel
});
}
}
if (response.tooltip) {
if (!this.tooltipModel) {
this.tooltipModel = new TooltipModel(response.tooltip, {
configModel: opts.configModel || this._configModel
});
}
}
if (response.options.table_name) {
// Set autostyle as false if it doesn't contain any id
attrs.autoStyle = attrs.autoStyle || false;
if (!this.styleModel) {
this.styleModel = new StyleDefinitionModel(response.options.style_properties, {
parse: true
});
this.cartocssModel = new StyleCartoCSSModel({
content: attrs.cartocss
}, {
history: response.options.cartocss_history || response.options.tile_style_history
});
}
if (!this.sqlModel) {
this.sqlModel = new DataSQLModel({
content: attrs.sql
}, {
history: response.options.sql_history
});
}
}
// Flatten the rest of the attributes
return attrs;
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
this.on('change:source change:sql', this._onPosibleLayerSchemaChanged, this);
if (this.styleModel) {
this.styleModel.bind('change:type change:animated', function () {
if (this.styleModel.isAggregatedType() || this.styleModel.isAnimation()) {
// setTemplate will clear fields
this.infowindowModel && this.infowindowModel.unsetTemplate();
this.tooltipModel && this.tooltipModel.unsetTemplate();
}
}, this);
}
},
save: function (attrs, options) {
attrs = attrs || {};
options = options || {};
// We assume that if the layer is saved, we have to disable autostyle
var autoStyleAttrs = {
autoStyle: false
};
// But if the layer is saved with shouldPreserveAutoStyle option, we should preserve autostyle
if (options && options.shouldPreserveAutoStyle) {
delete autoStyleAttrs.autoStyle;
} else if (this.get('autoStyle')) {
this.styleModel && this.styleModel.resetPropertiesFromAutoStyle();
}
attrs = _.extend(
{},
autoStyleAttrs,
attrs
);
return Backbone.Model.prototype.save.call(this, attrs, options);
},
toJSON: function () {
// Un-flatten the internal attrs to the datastructure that's expected by the API endpoint
var options = _.omit(this.attributes, ATTR_NAMES.concat(['cartocss', 'sql', 'autoStyle']));
// Map back internal attrs to the expected attrs names by the API endpoint
var cartocss = this.get('cartocss');
if (cartocss) {
options.tile_style = cartocss;
}
var sql = this.get('sql');
if (sql) {
options.query = sql;
}
var defaultAttributes = {
kind: layerTypesAndKinds.getKind(this.get('type')),
options: options
};
var infowindowData = this.infowindowModel && this.infowindowModel.toJSON();
if (!_.isEmpty(infowindowData)) {
defaultAttributes.infowindow = this.infowindowModel.toJSON();
}
var tooltipData = this.tooltipModel && this.tooltipModel.toJSON();
if (!_.isEmpty(tooltipData)) {
defaultAttributes.tooltip = this.tooltipModel.toJSON();
}
if (this.styleModel && !this.styleModel.isAutogenerated()) {
defaultAttributes.options.style_properties = this.styleModel.toJSON();
}
if (this.cartocssModel) {
defaultAttributes.options.cartocss_history = this.cartocssModel.getHistory();
}
if (this.sqlModel) {
defaultAttributes.options.sql_history = this.sqlModel.getHistory();
}
var attributes = _.omit(this.attributes, 'infowindow', 'tooltip', 'options', 'error', 'autoStyle');
return _.defaults(
defaultAttributes,
_.pick(attributes, ATTR_NAMES)
);
},
canBeDeletedByUser: function () {
return this.collection.getNumberOfDataLayers() > 1 && this.isDataLayer() &&
(this._canBeFoldedUnderAnotherLayer() || !this._isAllDataLayersDependingOnAnyAnalysisOfThisLayer());
},
isOwnerOfAnalysisNode: function (nodeModel) {
return nodeModel && nodeModel.letter() === this.get('letter');
},
ownedPrimaryAnalysisNodes: function () {
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
return this.isOwnerOfAnalysisNode(nodeDefModel)
? nodeDefModel.linkedListBySameLetter()
: [];
},
getName: function () {
return this.get('name') ||
this.get('table_name_alias') ||
this.get('table_name');
},
getTableName: function () {
return this.get('table_name') || '';
},
getColor: function () {
return layerColors.getColorForLetter(this.get('letter'));
},
containsNode: function (other) {
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
return nodeDefModel && nodeDefModel.containsNode(other);
},
getAnalysisDefinitionNodeModel: function () {
return this.findAnalysisDefinitionNodeModel(this.get('source'));
},
findAnalysisDefinitionNodeModel: function (id) {
return this.collection && this.collection.findAnalysisDefinitionNodeModel(id);
},
_onPosibleLayerSchemaChanged: function (eventName, attrs, options) {
// Used to avoid resetting styles on source_id changes when we have saved styles for the node
if (options && options.ignoreSchemaChange) {
return;
}
if (this.infowindowModel) {
this.infowindowModel.clearFields();
}
if (this.tooltipModel) {
this.tooltipModel.clearFields();
}
if (this.styleModel) {
this.styleModel.resetStyles();
}
},
toggleVisible: function () {
this.set('visible', !this.get('visible'));
},
toggleCollapse: function () {
this.set('collapsed', !this.get('collapsed'));
},
hasAnalyses: function () {
return this.getNumberOfAnalyses() > 0;
},
hasAggregatedStyles: function () {
return this.styleModel && this.styleModel.isAggregatedType();
},
getNumberOfAnalyses: function () {
var analysisNode = this.getAnalysisDefinitionNodeModel();
var count = 0;
while (analysisNode && this.isOwnerOfAnalysisNode(analysisNode)) {
analysisNode = analysisNode.getPrimarySource();
if (analysisNode) {
count += 1;
}
}
return count;
},
getQualifiedTableName: function () {
var userName = this.get('user_name') || this.collection.userModel.get('username');
return TableNameUtils.getQualifiedTableName(
this.getTableName(),
userName,
this.collection.userModel.isInsideOrg()
);
},
getColumnNamesFromSchema: function () {
return this._getQuerySchemaModel().getColumnNames();
},
_getQuerySchemaModel: function () {
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
return nodeDefModel.querySchemaModel;
},
isDataLayer: function () {
var layerType = this.get('type');
return layerTypesAndKinds.isCartoDBType(layerType) ||
layerTypesAndKinds.isTorqueType(layerType);
},
isTorqueLayer: function () {
return this.get('type') === 'torque';
},
isAutoStyleApplied: function () {
var autoStyle = this.get('autoStyle');
return (autoStyle != null && autoStyle !== false);
},
_canBeFoldedUnderAnotherLayer: function () {
var thisNodeDefModel = this.getAnalysisDefinitionNodeModel();
return this.collection.any(function (m) {
if (m !== this && m.isDataLayer()) {
var otherNodeDefModel = m.getAnalysisDefinitionNodeModel();
if (otherNodeDefModel === thisNodeDefModel) return true;
var lastNode = _.last(otherNodeDefModel.linkedListBySameLetter());
return lastNode.getPrimarySource() === thisNodeDefModel;
}
}, this);
},
_isAllDataLayersDependingOnAnyAnalysisOfThisLayer: function () {
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
if (!nodeDefModel) return false;
if (!this.isOwnerOfAnalysisNode(nodeDefModel)) return false;
var linkedNodesList = nodeDefModel.linkedListBySameLetter();
return this.collection.chain()
.filter(function (m) {
return m !== this && !!m.get('source');
}, this)
.all(function (m) {
return _.any(linkedNodesList, function (node) {
return m.containsNode(node);
});
}, this)
.value();
},
getAllDependentLayers: function () {
var self = this;
var layersCount = 0;
var layerDefinitionsCollectionModels = self.collection.models;
for (var i = 0; i < layerDefinitionsCollectionModels.length; i++) {
var layer = layerDefinitionsCollectionModels[i];
var dependentAnalysis = false;
if (layer !== self) {
var analysisNode = layer.getAnalysisDefinitionNodeModel();
while (analysisNode) {
if (self.isOwnerOfAnalysisNode(analysisNode)) {
dependentAnalysis = true;
}
analysisNode = analysisNode.getPrimarySource();
}
if (dependentAnalysis) {
layersCount += 1;
}
}
}
return layersCount;
},
matchesAttrs: function (otherAttrs) {
if (this.get('type') !== otherAttrs.type) {
return false;
}
if (layerTypesAndKinds.isTiledType(otherAttrs.type)) {
return this.get('name') === otherAttrs.name &&
this.get('urlTemplate') === otherAttrs.urlTemplate;
}
if (layerTypesAndKinds.isGMapsBase(otherAttrs.type)) {
return this.get('name') === otherAttrs.name &&
this.get('baseType') === otherAttrs.baseType &&
this.get('style') === otherAttrs.style;
}
if (layerTypesAndKinds.isPlainType(otherAttrs.type)) {
return this.get('color') === otherAttrs.color;
}
return false;
},
hasGeocodingAnalysisApplied: function () {
var analysisNode = this.getAnalysisDefinitionNodeModel();
if (analysisNode && analysisNode.get('type') === 'geocoding') {
return true;
}
while (analysisNode && this.isOwnerOfAnalysisNode(analysisNode)) {
analysisNode = analysisNode.getPrimarySource();
if (analysisNode && analysisNode.get('type') === 'geocoding') {
return true;
}
}
return false;
},
_hasAnyAnalysisApplied: function () {
var analysisNode = this.getAnalysisDefinitionNodeModel();
return analysisNode.get('type') !== 'source';
},
isEmpty: function () {
throw new Error('LayerDefinitionModel.isEmpty() is an async operation. Use `.isEmptyAsync` instead.');
},
isEmptyAsync: function () {
var nodeModel = this.getAnalysisDefinitionNodeModel();
var hasAnyAnalysisApplied = this._hasAnyAnalysisApplied();
var hasCustomQueryApplied = nodeModel.isCustomQueryApplied();
return new Promise(function (resolve, reject) {
nodeModel.queryRowsCollection.isEmptyAsync()
.then(function (isEmpty) {
resolve(!hasAnyAnalysisApplied && !hasCustomQueryApplied && isEmpty);
});
});
},
isDataFiltered: function () {
var nodeModel = this.getAnalysisDefinitionNodeModel();
var hasAnyAnalysisApplied = this._hasAnyAnalysisApplied();
var hasCustomQueryApplied = nodeModel.isCustomQueryApplied();
return new Promise(function (resolve, reject) {
nodeModel.queryRowsCollection.isEmptyAsync()
.then(function (isEmpty) {
resolve((hasAnyAnalysisApplied || hasCustomQueryApplied) && isEmpty);
});
});
},
isDone: function () {
var nodeModel = this.getAnalysisDefinitionNodeModel();
return nodeModel.queryRowsCollection.isDone() &&
nodeModel.queryGeometryModel.isDone() &&
nodeModel.querySchemaModel.isDone();
},
canBeGeoreferenced: function () {
var self = this;
var analysisDefinitionNodeModel = this.getAnalysisDefinitionNodeModel();
var emptyPromise = self.isEmptyAsync();
var geomPromise = analysisDefinitionNodeModel.queryGeometryModel.hasValueAsync();
return Promise.all([emptyPromise, geomPromise])
.then(function (values) {
var isEmpty = values[0];
var hasGeom = values[1];
var canBeGeoreferenced =
!isEmpty &&
!hasGeom &&
!self.hasGeocodingAnalysisApplied() &&
!self._hasAnyAnalysisApplied() &&
!analysisDefinitionNodeModel.isCustomQueryApplied();
return canBeGeoreferenced;
});
},
fetchQueryRowsIfRequired: function () {
var self = this;
var analysisDefinitionNodeModel = this.getAnalysisDefinitionNodeModel();
analysisDefinitionNodeModel.queryGeometryModel.hasValueAsync()
.then(function (geomHasValue) {
if (!self.hasGeocodingAnalysisApplied() &&
!geomHasValue &&
!analysisDefinitionNodeModel.isCustomQueryApplied() &&
analysisDefinitionNodeModel.queryRowsCollection.isUnavailable()) {
analysisDefinitionNodeModel.queryRowsCollection.fetch();
}
});
}
});

View File

@@ -0,0 +1,411 @@
var $ = require('jquery');
var _ = require('underscore');
var Backbone = require('backbone');
var LayerDefinitionModel = require('./layer-definition-model');
var layerLetters = require('./layer-letters');
var layerColors = require('./layer-colors');
var nodeIds = require('builder/value-objects/analysis-node-ids');
var layerTypesAndKinds = require('./layer-types-and-kinds');
var SQLUtils = require('builder/helpers/sql-utils');
var TableNameUtils = require('builder/helpers/table-name-utils');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'analysisDefinitionNodesCollection',
'configModel',
'mapId',
'stateDefinitionModel',
'userModel'
];
var generateLabelsLayerName = function (layerName) {
return layerName + ' Labels';
};
/**
* Collection of layer definitions
*/
module.exports = Backbone.Collection.extend({
initialize: function (models, opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
},
url: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v1/maps/' + this._mapId + '/layers';
},
parse: function (r) {
return r.layers;
},
save: function (options) {
this.each(function (layerDefModel, index) {
layerDefModel.set('order', index, { silent: true });
});
Backbone.sync('update', this, options);
},
toJSON: function () {
return {
layers: Backbone.Collection.prototype.toJSON.apply(this, arguments)
};
},
/**
* Intended to be called from entry point, to make sure initial layers are taken into account
*/
resetByLayersData: function (layersData) {
this.reset(layersData, {
silent: true,
initialLetters: _.chain(layersData)
.pluck('options')
.pluck('letter')
.compact()
.value()
});
this._sanitizeLabels();
},
comparator: function (model) {
return model.get('order');
},
model: function (d, opts) {
var self = opts.collection;
// Add data required for new editor if not set (e.g. a vis created on old editor doesn't contain letter and source)
var o = _.clone(d.options) || {};
var attrs = _.defaults(
{ options: o },
_.omit(d, ['options']
));
if (!isNaN(opts.at)) {
attrs.options.order = opts.at;
}
if (layerTypesAndKinds.isKindDataLayer(attrs.kind)) {
o.letter = o.letter || layerLetters.next(self._takenLetters(opts.initialLetters));
o.color = o.color || layerColors.getColorForLetter(o.letter);
// Create source attr if it does not already exist
var sourceId = nodeIds.next(o.letter);
if (o.table_name && (!o.source || o.source === sourceId)) {
var tableName = TableNameUtils.getUnqualifiedName(o.table_name);
var userName = o.user_name || TableNameUtils.getUsername(o.table_name);
var qualifyTableName = (userName && self._configModel.get('user_name') !== userName) || self._userModel.isInsideOrg();
tableName = TableNameUtils.getQualifiedTableName(o.table_name, userName, qualifyTableName);
o.source = o.source || sourceId;
o.query = o.query || SQLUtils.getDefaultSQLFromTableName(tableName);
// Add analysis definition unless already existing
self._analysisDefinitionNodesCollection.createSourceNode({
id: sourceId,
tableName: tableName,
query: o.query
});
}
}
var parseAttrs = typeof opts.parse === 'boolean' ? opts.parse : true;
var m = new LayerDefinitionModel(attrs, {
parse: parseAttrs,
collection: self,
configModel: self._configModel
});
return m;
},
nextLetter: function () {
return layerLetters.next(this._takenLetters());
},
findAnalysisDefinitionNodeModel: function (id) {
return this._analysisDefinitionNodesCollection.get(id);
},
findOwnerOfAnalysisNode: function (nodeDefModel) {
return this.find(function (layerDefModel) {
return layerDefModel.isOwnerOfAnalysisNode(nodeDefModel);
});
},
findPrimaryParentLayerToAnalysisNode: function (nodeDefModel, opts) {
if (!nodeDefModel) return;
opts = opts || {};
if (!_.isArray(opts.exclude)) {
opts.exclude = [opts.exclude];
}
var owner = this.findOwnerOfAnalysisNode(nodeDefModel);
return this.find(function (layerDefModel) {
if (layerDefModel === owner) return;
if (_.contains(opts.exclude, layerDefModel)) return;
var layerHeadNode = layerDefModel.getAnalysisDefinitionNodeModel();
if (nodeDefModel === layerHeadNode) return true;
if (layerHeadNode) {
var primarySourceOfLastOwnNode = _.last(layerHeadNode.linkedListBySameLetter()).getPrimarySource();
return nodeDefModel === primarySourceOfLastOwnNode;
}
});
},
isThereAnyGeometryData: function () {
var dataLayers = this._getDataLayers();
var allQueryGeometryModels = this._getAllQueryGeometryModels(dataLayers);
var queryGeometryModelsValuePromises = _.map(allQueryGeometryModels, function (queryGeometryModel) {
return queryGeometryModel.hasValueAsync();
});
return Promise.all(queryGeometryModelsValuePromises)
.then(function (hasGeomValues) {
return _.some(hasGeomValues);
});
},
loadAllQueryGeometryModels: function (callback) {
var promises = this.filter(function (layerDefModel) {
return layerDefModel.isDataLayer();
}).map(function (layerDefModel) {
var queryGeometryModel = layerDefModel.getAnalysisDefinitionNodeModel().queryGeometryModel;
var status = queryGeometryModel.get('status');
var deferred = new $.Deferred();
if (queryGeometryModel.isFetched()) {
deferred.resolve();
} else if (queryGeometryModel.canFetch()) {
if (status !== 'fetching') {
queryGeometryModel.fetch({
success: function () {
deferred.resolve();
},
error: function () {
deferred.reject();
}
});
} else {
deferred.resolve();
}
} else {
deferred.reject();
}
return deferred.promise();
}, this);
$.when.apply($, promises).done(callback);
},
isThereAnyTorqueLayer: function () {
return this.any(function (model) {
return layerTypesAndKinds.isTorqueType(model.get('type'));
});
},
isThereAnyCartoDBLayer: function () {
return this.any(function (model) {
return layerTypesAndKinds.isCartoDBType(model.get('type'));
});
},
anyContainsNode: function (nodeDefModel) {
return this.any(function (layerDefModel) {
return layerDefModel.containsNode(nodeDefModel);
});
},
isDataLayerOnTop: function (lyrModel) {
var currentPos = this.indexOf(lyrModel);
return currentPos === this.getTopDataLayerIndex();
},
getTopDataLayerIndex: function () {
var layerOnTop = this.getLayerOnTop();
var at = this.indexOf(layerOnTop);
var hasLabels = layerOnTop &&
!layerTypesAndKinds.isCartoDBType(layerOnTop.get('type')) &&
!layerTypesAndKinds.isTorqueType(layerOnTop.get('type'));
if (hasLabels) { at--; }
return at;
},
getNumberOfDataLayers: function () {
return this.select(function (model) {
return layerTypesAndKinds.isTypeDataLayer(model.get('type'));
}).length;
},
_takenLetters: function (otherLetters) {
var valuesFromAddedModels = _.compact(this.pluck('letter'));
// When adding multiple items the models created so far are stored in the internal object this._byId,
// need to make sure to take them into account when returning already taken letter.
var valuesFromModelsNotYetAdded = _.chain(this._byId).values().invoke('get', 'letter').value();
return _.union(valuesFromAddedModels, valuesFromModelsNotYetAdded, otherLetters);
},
// This ensures that we don't end up with more than one labels layer
_sanitizeLabels: function () {
var troubled = this.models.filter(function (layer) {
return layerTypesAndKinds.isTiledType(layer.get('type'));
}).filter(function (layer) {
var index = this.models.indexOf(layer);
return (index > 0 && index < this.length - 1);
}.bind(this));
troubled.length > 0 && _.each(troubled, function (layer) {
layer.destroy();
});
},
setBaseLayer: function (newBaseLayerAttrs) {
this.trigger('changingBaseLayer');
newBaseLayerAttrs = _.clone(newBaseLayerAttrs);
if (this.isBaseLayerAdded(newBaseLayerAttrs)) {
this.trigger('baseLayerChanged');
return false;
}
var newBaseLayerHasLabels = !!(newBaseLayerAttrs.labels && newBaseLayerAttrs.labels.urlTemplate);
var labelsLayerOptions = {
silent: true,
wait: true, // do not add/remove the layer unless it's created/removed/updated successfully
success: function () {
this._sanitizeLabels();
this.trigger('baseLayerChanged');
}.bind(this),
error: function () {
this.trigger('baseLayerFailed');
}.bind(this)
};
var baseLayerOptions = {
silent: true,
wait: true, // do not update the layer unless it's created/updated successfully
success: function () {
var labelsLayer = this.getLabelsLayer();
if (newBaseLayerHasLabels) {
if (labelsLayer) {
this._updateLabelsLayer(newBaseLayerAttrs, labelsLayerOptions);
} else {
this._addLabelsLayer(newBaseLayerAttrs, labelsLayerOptions);
}
} else {
if (labelsLayer) {
this._destroyLabelsLayer(labelsLayerOptions);
} else {
this._sanitizeLabels();
this.trigger('baseLayerChanged');
}
}
}.bind(this),
error: function () {
this.trigger('baseLayerFailed');
}.bind(this)
};
if (this.getBaseLayer()) {
this._updateBaseLayer(newBaseLayerAttrs, baseLayerOptions);
} else {
this._createBaseLayer(newBaseLayerAttrs, baseLayerOptions);
}
},
isBaseLayerAdded: function (basemapAttrs) {
var baseLayerDefinitionModel = this.getBaseLayer();
return baseLayerDefinitionModel.matchesAttrs(basemapAttrs);
},
_updateBaseLayer: function (basemapAttrs, opts) {
var currentBaseLayer = this.getBaseLayer();
var newBaseLayerAttrs = _.omit(basemapAttrs, [ 'id', 'order', 'labels', 'template' ]);
currentBaseLayer.attributes = _.extend({
order: 0
}, _.pick(currentBaseLayer.attributes, [
'id',
'type',
'category'
]));
currentBaseLayer.save(newBaseLayerAttrs, opts);
},
_createBaseLayer: function (newBaseLayerAttrs, opts) {
newBaseLayerAttrs = _.extend(newBaseLayerAttrs, {
order: 0
});
return this.create(newBaseLayerAttrs, _.extend({ at: 0 }, opts));
},
_updateLabelsLayer: function (newBaseLayerAttrs, opts) {
var labelsLayer = this.getLabelsLayer();
var newAttrs = _.omit(newBaseLayerAttrs, 'labels');
newAttrs = _.pick(newAttrs, _.identity);
newAttrs = _.extend(newAttrs, newBaseLayerAttrs.labels, {
name: generateLabelsLayerName(newBaseLayerAttrs.name)
});
labelsLayer.save(newAttrs, opts);
},
_addLabelsLayer: function (newBaseLayerAttrs, opts) {
var optionsAttribute = _.pick(newBaseLayerAttrs, ['type', 'subdomains', 'minZoom', 'maxZoom', 'name', 'className', 'attribution', 'category']);
optionsAttribute = _.extend(optionsAttribute, newBaseLayerAttrs.labels, {
name: generateLabelsLayerName(newBaseLayerAttrs.name)
});
var labelsLayerAttrs = {
order: this.getLayerOnTop().get('order') + 1,
options: optionsAttribute
};
this.create(labelsLayerAttrs, opts);
},
_destroyLabelsLayer: function (opts) {
this.getLayerOnTop().destroy(opts);
},
_getAllQueryGeometryModels: function (dataLayers) {
return _.compact(_.map(dataLayers, function (dataLayer) {
return dataLayer.getAnalysisDefinitionNodeModel().queryGeometryModel;
}));
},
_getDataLayers: function () {
return this.filter(function (layerDefinitionModel) {
return layerDefinitionModel.isDataLayer();
});
},
getBaseLayer: function () {
return this.first();
},
getLabelsLayer: function () {
var layerOnTop = this.getLayerOnTop();
if (this.size() > 1 && layerTypesAndKinds.isTiledType(layerOnTop.get('type'))) {
return layerOnTop;
}
},
getLayerOnTop: function () {
return this.last();
}
});

View File

@@ -0,0 +1,23 @@
var _ = require('underscore');
module.exports = {
/**
* Get the next available letter.
* @param {Array} letters e.g. ['a', 'b', 'd']
* @return {String} e.g. 'c'
*/
next: function (letters) {
return _.chain(letters)
.sort()
.reduce(function (memo, letter) {
if (letter === memo) {
return String.fromCharCode(letter.charCodeAt() + 1);
} else {
return memo;
}
}, 'a')
.value();
}
};

View File

@@ -0,0 +1,73 @@
// Types
var PLAIN = 'Plain';
var CARTODB = 'CartoDB';
var TILED = 'Tiled';
var TORQUE = 'torque';
var GMAPSBASE = 'GMapsBase';
var WMS = 'WMS';
// Values of `kind` attribute that the backend/DB knows about
var KIND_TO_TYPE_MAP = {
'background': PLAIN,
'carto': CARTODB,
'gmapsbase': GMAPSBASE,
'tiled': TILED,
'torque': TORQUE,
'wms': WMS
};
module.exports = {
getType: function (kind) {
var type = KIND_TO_TYPE_MAP[kind];
if (!type) {
throw new Error('no type found for given kind ' + kind);
}
return type;
},
getKind: function (type) {
if (!type) {
throw new Error('no kind found for given type ' + type);
}
var kind;
for (kind in KIND_TO_TYPE_MAP) {
if (KIND_TO_TYPE_MAP[kind] === type) {
break;
}
}
return kind;
},
isKindDataLayer: function (kind) {
var type = KIND_TO_TYPE_MAP[kind];
return this.isTypeDataLayer(type);
},
isTypeDataLayer: function (type) {
return this.isCartoDBType(type) || this.isTorqueType(type);
},
isCartoDBType: function (type) {
return type === CARTODB;
},
isTorqueType: function (type) {
return type === TORQUE;
},
isTiledType: function (type) {
return type === TILED;
},
isPlainType: function (type) {
return type === PLAIN;
},
isGMapsBase: function (type) {
return type === GMAPSBASE;
}
};

View File

@@ -0,0 +1,110 @@
var Backbone = require('backbone');
var _ = require('underscore');
var syncAbort = require('builder/data/backbone/sync-abort');
var LegendsState = require('./legends-state');
/*
Base model for a legend. It should have a reference to a layerDefinitionModel.
*/
module.exports = Backbone.Model.extend({
defaults: {
type: 'none',
title: '',
postHTMLSnippet: '',
preHTMLSnippet: ''
},
sync: syncAbort,
parse: function (r) {
var attrs = _.extend({},
_.omit(r, 'created_at', 'updated_at', 'definition', 'pre_html', 'post_html')
);
if (r.conf && r.conf.columns) {
attrs.customState = r.conf.columns;
}
attrs.preHTMLSnippet = r.pre_html;
attrs.postHTMLSnippet = r.post_html;
return attrs;
},
urlRoot: function () {
var baseUrl = this.configModel.get('base_url');
return baseUrl + '/api/v3/viz/' + this.vizId + '/layer/' + this.layerDefinitionModel.id + '/legends';
},
initialize: function (attrs, opts) {
if (!opts.layerDefinitionModel) throw new Error('layerDefinitionModel is required');
if (!opts.configModel) throw new Error('configModel is required');
if (!opts.vizId) throw new Error('vizId is required');
this.layerDefinitionModel = opts.layerDefinitionModel;
this.configModel = opts.configModel;
this.vizId = opts.vizId;
},
getStyles: function () {
return this.layerDefinitionModel && this.layerDefinitionModel.styleModel || null;
},
fetch: function () {
throw new Error('This model should not make any fetch calls. It should be created from the vizJSON.');
},
getType: function () {
return this.get('type');
},
toJSON: function () {
return _.extend(
this.attributes,
{
type: this.getType()
}
);
},
getAttributes: function () {
return _.extend(
{},
_.pick(this.attributes, _.keys(this.defaults)),
_.omit(this.attributes, 'pre_html', 'post_html', 'customState')
);
},
setAttributes: function (attrs) {
// Store the real values
LegendsState.set(this.layerDefinitionModel, this.get('type') || attrs.type, attrs);
this.set(attrs);
},
// Overriding set
set: function (key, val, options) {
var attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
var layerDefinitionModel = this.layerDefinitionModel || options.layerDefinitionModel;
var type = this.get('type') || attrs.type;
// Override attributes with the current custom state
var state = LegendsState.get(layerDefinitionModel, type);
var attributes = _.extend({}, attrs, {
customState: _.keys(state)
}, state);
return Backbone.Model.prototype.set.call(this, attributes, options);
},
hasCustomHtml: function () {
return false;
}
});

View File

@@ -0,0 +1,65 @@
var _ = require('underscore');
var LegendBaseDefModel = require('./legend-base-definition-model');
var LegendColorHelper = require('builder/editor/layers/layer-content-views/legend/form/legend-color-helper');
var DEFAULT_BUBBLES_COLOR = '#999999';
module.exports = LegendBaseDefModel.extend({
defaults: _.extend({}, LegendBaseDefModel.prototype.defaults,
{
type: 'bubble',
fillColor: DEFAULT_BUBBLES_COLOR,
topLabel: '',
bottomLabel: ''
}
),
parse: function (r, opts) {
var attrs = LegendBaseDefModel.prototype.parse.call(this, r);
if (opts.layerDefinitionModel) {
if (r.definition) {
attrs.fillColor = r.definition.color;
attrs.topLabel = r.definition.top_label;
attrs.bottomLabel = r.definition.bottom_label;
} else {
attrs.fillColor = this._inheritStyleColor(opts.layerDefinitionModel.styleModel);
}
}
return attrs;
},
toJSON: function () {
return _.extend(
{},
_.omit(this.attributes, 'fill', 'fillColor', 'topLabel', 'bottomLabel', 'postHTMLSnippet', 'preHTMLSnippet', 'customState'),
{
pre_html: this.get('preHTMLSnippet'),
post_html: this.get('postHTMLSnippet')
},
{
conf: {
columns: this.get('customState')
}
},
{
definition: {
color: this.get('fillColor'),
top_label: this.get('topLabel'),
bottom_label: this.get('bottomLabel')
}
}
);
},
_inheritStyleColor: function (styleModel) {
var fill = styleModel.get('fill');
var stroke = styleModel.get('stroke');
var color = (fill && fill.color) || (stroke && stroke.color);
if (color && (color.fixed || color.range)) {
return LegendColorHelper.getBubbles(color).color.fixed;
} else {
return DEFAULT_BUBBLES_COLOR;
}
}
});

View File

@@ -0,0 +1,26 @@
var _ = require('underscore');
var LegendBaseDefModel = require('./legend-base-definition-model');
module.exports = LegendBaseDefModel.extend({
defaults: _.extend({}, LegendBaseDefModel.prototype.defaults,
{
type: 'category'
}
),
toJSON: function () {
return _.extend(
{},
_.omit(this.attributes, 'postHTMLSnippet', 'preHTMLSnippet', 'customState'),
{
pre_html: this.get('preHTMLSnippet'),
post_html: this.get('postHTMLSnippet')
},
{
conf: {
columns: this.get('customState')
}
}
);
}
});

View File

@@ -0,0 +1,51 @@
var _ = require('underscore');
var LegendBaseDefModel = require('./legend-base-definition-model');
module.exports = LegendBaseDefModel.extend({
defaults: _.extend({}, LegendBaseDefModel.prototype.defaults,
{
type: 'choropleth',
prefix: '',
suffix: '',
leftLabel: '',
rightLabel: ''
}
),
parse: function (r, opts) {
var attrs = LegendBaseDefModel.prototype.parse.call(this, r);
if (r.definition) {
attrs.prefix = r.definition.prefix;
attrs.suffix = r.definition.suffix;
attrs.leftLabel = r.definition.left_label;
attrs.rightLabel = r.definition.right_label;
}
return attrs;
},
toJSON: function () {
var definition = {
prefix: this.get('prefix'),
suffix: this.get('suffix'),
left_label: this.get('leftLabel'),
right_label: this.get('rightLabel')
};
return _.extend(
{},
_.omit(this.attributes, 'prefix', 'suffix', 'leftLabel', 'rightLabel', 'postHTMLSnippet', 'preHTMLSnippet', 'customState'),
{
pre_html: this.get('preHTMLSnippet'),
post_html: this.get('postHTMLSnippet')
},
{
conf: {
columns: this.get('customState')
}
},
{
definition: _.pick(definition, _.identity)
}
);
}
});

View File

@@ -0,0 +1,84 @@
var _ = require('underscore');
var LegendBaseDefModel = require('./legend-base-definition-model');
module.exports = LegendBaseDefModel.extend({
defaults: _.extend({}, LegendBaseDefModel.prototype.defaults,
{
type: 'custom_choropleth',
prefix: '',
suffix: '',
colors: [],
leftLabel: '',
rightLabel: ''
}
),
parse: function (r, opts) {
var attrs = LegendBaseDefModel.prototype.parse.call(this, r);
var fill;
var stroke;
var color;
if (r.definition) {
attrs.colors = r.definition.colors;
attrs.prefix = r.definition.prefix;
attrs.suffix = r.definition.suffix;
attrs.leftLabel = r.definition.left_label;
attrs.rightLabel = r.definition.right_label;
} else {
fill = opts.layerDefinitionModel.styleModel.get('fill');
stroke = opts.layerDefinitionModel.styleModel.get('stroke');
color = fill.color || stroke.color;
attrs.colors = this._inheritStyleColors(color);
}
return attrs;
},
toJSON: function () {
var definition = {
prefix: this.get('prefix'),
suffix: this.get('suffix'),
left_label: this.get('leftLabel'),
right_label: this.get('rightLabel'),
colors: this.get('colors').map(function (item) {
return {
color: item.color
};
})
};
return _.extend(
{},
_.omit(this.attributes, 'prefix', 'suffix', 'leftLabel', 'rightLabel', 'colors', 'postHTMLSnippet', 'preHTMLSnippet', 'customState'),
{
pre_html: this.get('preHTMLSnippet'),
post_html: this.get('postHTMLSnippet')
},
{
conf: {
columns: this.get('customState')
}
},
{
definition: _.pick(definition, _.identity)
}
);
},
_inheritStyleColors: function (color) {
var range = 0;
var colors = [];
if (color.range) {
range = color.range.length;
colors = _.range(range).map(function (v, index) {
return {
color: color.range[index]
};
});
}
return colors;
}
});

View File

@@ -0,0 +1,63 @@
var _ = require('underscore');
var LegendBaseDefModel = require('./legend-base-definition-model');
var StyleHelper = require('builder/helpers/style');
module.exports = LegendBaseDefModel.extend({
defaults: _.extend({}, LegendBaseDefModel.prototype.defaults,
{
type: 'custom',
items: [],
html: ''
}
),
parse: function (r, opts) {
var attrs = LegendBaseDefModel.prototype.parse.call(this, r);
if (r.definition) {
attrs.items = r.definition.categories;
attrs.html = r.definition.html || '';
} else {
attrs.items = StyleHelper.getStyleCategories(opts.layerDefinitionModel.styleModel);
}
attrs.type = 'custom';
return attrs;
},
toJSON: function () {
var definition = {
categories: this.get('items').map(function (item) {
return {
title: (item.title || '').toString(),
color: item.color,
icon: item.image || item.icon
};
})
};
if (this.hasCustomHtml()) {
definition.html = this.get('html');
}
return _.extend(
{},
_.omit(this.attributes, 'items', 'html', 'postHTMLSnippet', 'preHTMLSnippet', 'customState'),
{
pre_html: this.get('preHTMLSnippet'),
post_html: this.get('postHTMLSnippet')
},
{
conf: {
columns: this.get('customState')
}
},
{
definition: definition
}
);
},
hasCustomHtml: function () {
return this.get('html') !== '';
}
});

View File

@@ -0,0 +1,104 @@
var Backbone = require('backbone');
var _ = require('underscore');
var LegendBaseDefModel = require('./legend-base-definition-model');
var layerTypesAndKinds = require('builder/data/layer-types-and-kinds');
var CategoryModel = require('./legend-category-definition-model');
var BubbleModel = require('./legend-bubble-definition-model');
var ChoroplethModel = require('./legend-choropleth-definition-model');
var CustomChoroplethModel = require('./legend-custom-choropleth-definition-model');
var CustomModel = require('./legend-custom-definition-model');
var TorqueModel = require('./legend-torque-definition-model');
var LEGEND_MODEL_TYPE = {
'bubble': BubbleModel,
'category': CategoryModel,
'choropleth': ChoroplethModel,
'custom_choropleth': CustomChoroplethModel,
'custom': CustomModel,
'html': CustomModel,
'torque': TorqueModel
};
module.exports = Backbone.Collection.extend({
model: LegendBaseDefModel,
initialize: function (models, options) {
if (!options.configModel) throw new Error('configModel is required');
if (!options.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!options.vizId) throw new Error('vizId is required');
this.configModel = options.configModel;
this.layerDefinitionsCollection = options.layerDefinitionsCollection;
this.vizId = options.vizId;
},
/**
* Intended to be called from entry point, to make sure initial layers are taken into account
*/
resetByData: function (vizJSON) {
var models = [];
var layers = _.filter(vizJSON.layers, function (layer) {
return this._isDataLayer(layer.type);
}, this);
_.each(layers, function (layer) {
var layerDefModel = this._findLayerDefinitionModel(layer.id);
var legends = [];
if (layer.legends) {
legends = layer.legends.map(function (legend) {
var Klass = LEGEND_MODEL_TYPE[legend.type];
return new Klass(legend, {
layerDefinitionModel: layerDefModel,
configModel: this.configModel,
vizId: this.vizId,
parse: true
});
}, this);
}
models.push(legends);
}, this);
this.reset(_.flatten(models));
},
fetch: function () {
throw new Error('This collection should not make any fetch calls. It should be populated from the vizJSON.');
},
_isDataLayer: function (layerType) {
return layerTypesAndKinds.isCartoDBType(layerType) ||
layerTypesAndKinds.isTorqueType(layerType);
},
_findLayerDefinitionModel: function (id) {
return this.layerDefinitionsCollection.findWhere({id: id});
},
findByLayerDefModel: function (layerDefModel) {
var id = layerDefModel.id;
return this.filter(function (legendDefModel) {
var layerDefModel = legendDefModel.layerDefinitionModel;
return layerDefModel.id === id;
});
},
findByLayerDefModelAndType: function (layerDefModel, type) {
var id = layerDefModel.id;
return this.find(function (legendDefModel) {
var layerDefModel = legendDefModel.layerDefinitionModel;
return layerDefModel.id === id && legendDefModel.get('type') === type;
});
},
findByLayerDefModelAndTypes: function (layerDefModel, types) {
var id = layerDefModel.id;
return this.filter(function (legendDefModel) {
var layerDefModel = legendDefModel.layerDefinitionModel;
var type = legendDefModel.get('type');
return layerDefModel.id === id && types.indexOf(type) !== -1;
});
}
});

View File

@@ -0,0 +1,20 @@
var _ = require('underscore');
var LegendBaseDefModel = require('./legend-custom-definition-model');
module.exports = LegendBaseDefModel.extend({
defaults: function () {
return _.extend({}, LegendBaseDefModel.prototype.defaults,
{
type: 'torque',
items: [],
html: ''
}
);
},
parse: function (r, opts) {
var attrs = LegendBaseDefModel.prototype.parse.call(this, r, opts);
attrs.type = 'torque';
return attrs;
}
});

View File

@@ -0,0 +1,19 @@
var LEGENDS_METADATA = {
bubble: {
legendType: 'size'
},
category: {
legendType: 'color'
},
choropleth: {
legendType: 'color'
},
custom: {
legendType: 'color'
},
custom_choropleth: {
legendType: 'color'
}
};
module.exports = LEGENDS_METADATA;

View File

@@ -0,0 +1,72 @@
var _ = require('underscore');
var legendsMetadata = require('./legends-metadata');
var CUSTOM_ATTRIBUTES = ['title'];
var instance;
var legends;
var State = (function () {
var getType = function (type) {
var legend = legendsMetadata[type];
return legend && legend.legendType;
};
var onLayerAdded = function (layer) {
instance[layer.id] = {};
};
var onLayerRemoved = function (layer) {
delete instance[layer.id];
};
return {
init: function (layersDefinitionCollection, legendsDefinitionCollection) {
if (!instance) {
instance = {};
layersDefinitionCollection.on('add', onLayerAdded);
layersDefinitionCollection.on('remove', onLayerRemoved);
legends = legendsDefinitionCollection;
this.reset();
}
},
reset: function () {
legends.each(function (legend) {
var layer = legend.layerDefinitionModel;
if (!instance[layer.id]) {
instance[layer.id] = {};
}
var legendType = getType(legend.get('type'));
if (!instance[layer.id][legendType]) {
instance[layer.id][legendType] = {};
}
var state = _.pick(legend.attributes, legend.attributes.conf.columns);
instance[layer.id][legendType] = state;
});
},
set: function (layer, type, attrs) {
var attributes = _.pick(attrs, CUSTOM_ATTRIBUTES);
var legendType = getType(type);
if (instance && legendType) {
if (!instance[layer.id]) {
instance[layer.id] = {};
}
instance[layer.id][legendType] = attributes;
}
},
get: function (layer, type) {
var legendType = getType(type);
return instance && legendType && instance[layer.id] && instance[layer.id][legendType];
},
getInstance: function () {
return instance;
}
};
})();
module.exports = State;

View File

@@ -0,0 +1,101 @@
var Backbone = require('backbone');
var _ = require('underscore');
var OWN_ATTRS = ['drag', 'id', 'keyboard'];
var EMBED_ATTRS = ['legends', 'scrollwheel', 'layer_selector', 'dashboard_menu'];
var basemapProvidersAndCategories = require('./basemap-providers-and-categories');
/**
* Model that represents a visualization's Map
*/
module.exports = Backbone.Model.extend({
defaults: {
drag: true,
keyboard: true,
cartodb_logo: false,
legends: true,
layer_selector: false,
dashboard_menu: true,
scrollwheel: true
},
parse: function (r) {
return _.extend({
provider: r['map_provider']
},
_.pick(r, OWN_ATTRS),
_.pick(r.options, EMBED_ATTRS)
);
},
urlRoot: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v2/maps/';
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!opts.userModel) throw new Error('userModel is required');
this._configModel = opts.configModel;
this._userModel = opts.userModel;
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
this._layerDefinitionsCollection.bind('baseLayerChanged', this._onBaseLayerChanged, this);
},
_onBaseLayerChanged: function () {
var baseLayer = this._layerDefinitionsCollection.getBaseLayer();
var newProvider = basemapProvidersAndCategories.getProvider(baseLayer.get('type'));
this.save({
provider: newProvider,
minZoom: baseLayer.get('minZoom'),
maxZoom: baseLayer.get('maxZoom')
});
},
toJSON: function () {
return _.extend(
{},
_.omit(this.attributes, EMBED_ATTRS),
{
attribution: _.unique(this._layerDefinitionsCollection.pluck('attribution')),
user_id: this._userModel.get('id')
},
{
options: _.pick(this.attributes, EMBED_ATTRS)
}
);
},
setMapViewSize: function (size) {
this._mapViewSize = size;
},
getMapViewSize: function () {
return this._mapViewSize;
},
setStaticImageURLTemplate: function (staticImageURLTemplate) {
this._staticImageURLTemplate = staticImageURLTemplate;
},
getStaticImageURLTemplate: function () {
return this._staticImageURLTemplate;
},
setImageExportMetadata: function (metadata) {
this._imageExportMetadata = metadata;
},
getImageExportMetadata: function () {
return this._imageExportMetadata;
},
setConverters: function (converters) {
_.each(converters, function (converter, key) {
this[key] = converter;
}, this);
}
});

View File

@@ -0,0 +1,55 @@
module.exports = function (overlaysCollection, mapDefModel, userModel) {
var overlays = overlaysCollection.pluck('type');
var canRemoveLogo = userModel.get('actions').remove_logo;
return [{
setting: 'search',
label: _t('editor.settings.preview.options.elements.search'),
related: 'overlays',
disabled: false,
enabler: overlays.indexOf('search') > 0
},
{
setting: 'zoom',
label: _t('editor.settings.preview.options.elements.zoom'),
related: 'overlays',
disabled: false,
enabler: overlays.indexOf('zoom') > 0
},
{
setting: 'logo',
label: _t('editor.settings.preview.options.elements.logo'),
related: 'overlays',
disabled: false,
hidden: !canRemoveLogo,
enabler: overlays.indexOf('logo') > 0
},
{
setting: 'legends',
label: _t('editor.settings.preview.options.elements.legends'),
related: 'map',
disabled: false,
enabler: mapDefModel.get('legends') === true
},
{
setting: 'layer_selector',
label: _t('editor.settings.preview.options.elements.layer_selector'),
related: 'map',
disabled: false,
enabler: mapDefModel.get('layer_selector') === true
},
{
setting: 'dashboard_menu',
label: _t('editor.settings.preview.options.elements.dashboard_menu'),
related: 'map',
disabled: false,
enabler: mapDefModel.get('dashboard_menu') === true
},
{
setting: 'scrollwheel',
label: _t('editor.settings.preview.options.elements.scrollwheel'),
related: 'map',
disabled: false,
enabler: mapDefModel.get('scrollwheel') === true
}];
};

View File

@@ -0,0 +1,9 @@
var Backbone = require('backbone');
module.exports = Backbone.Collection.extend({
initialize: function (models, opts) {
if (!opts.visDefinitionModel) throw new Error('visDefinitionModel is required');
this.url = opts.visDefinitionModel.mapcapsURL();
}
});

View File

@@ -0,0 +1,31 @@
var Backbone = require('backbone');
var AssetModel = require('./asset-model');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'configModel',
'orgId'
];
module.exports = Backbone.Collection.extend({
model: AssetModel,
url: function (method) {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('organization-assets', method);
return baseUrl + '/api/' + version + '/organization/' + this._orgId + '/assets';
},
initialize: function (models, opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
},
deselectAll: function (m) {
this.each(function (mdl) {
if (mdl !== m && mdl.get('state') === 'selected') {
mdl.set('state', '');
}
});
}
});

View File

@@ -0,0 +1,40 @@
var Backbone = require('backbone');
var _ = require('underscore');
/**
* Organization info model
*
*/
module.exports = Backbone.Model.extend({
url: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v1/org';
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
if (!_.isEmpty(this.get('owner'))) {
this._ownerModel = new Backbone.Model(
_.omit(this.get('owner'), 'organization')
);
}
},
getOwnerId: function () {
return this._ownerModel && this._ownerModel.get('id');
},
getOwnerEmail: function () {
return this._ownerModel && this._ownerModel.get('email');
},
isOrgAdmin: function (user) {
return this.getOwnerId() === user.get('id') || !!_.find(this.get('admins'), function (u) {
return u.id === user.get('id');
});
}
});

View File

@@ -0,0 +1,83 @@
var Backbone = require('backbone');
module.exports = Backbone.Collection.extend({
initialize: function (models, opts) {
if (!opts.configModel) throw new Error('configModel is required');
if (!opts.visId) throw new Error('visId is required');
this._configModel = opts.configModel;
this._visId = opts.visId;
},
url: function () {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('overlays');
return baseUrl + '/api/' + version + '/viz/' + this._visId + '/overlays';
},
hasFetched: function () {
return this.length > 0;
},
removeOverlay: function (type) {
var overlay = this.findWhere({type: type});
overlay && overlay.destroy();
},
createOverlay: function (type) {
var types = {
'fullscreen': this._createFullScreenOverlay,
'layer_selector': this._createLayerSelectorOverlay,
'search': this._createSearchOverlay,
'zoom': this._createZoomOverlay,
'logo': this._createLogoOverlay
};
var fn = types[type];
fn && fn.call(this);
},
_createZoomOverlay: function () {
var options = {
type: 'zoom',
order: 6,
display: true,
x: 20,
y: 20
};
this.create(options);
},
_createLogoOverlay: function () {
var options = {
type: 'logo',
order: 10,
display: true,
x: 10,
y: 40
};
this.create(options);
},
_createSearchOverlay: function () {
var options = {
type: 'search',
order: 3,
display: true,
x: 60,
y: 20
};
this.create(options);
},
_createFullScreenOverlay: function () {
var options = {
type: 'fullscreen',
order: 7,
display: true,
x: 20,
y: 172
};
this.create(options);
}
});

View File

@@ -0,0 +1,303 @@
var Backbone = require('backbone');
var _ = require('underscore');
var Utils = require('builder/helpers/utils');
var UserModel = require('./user-model');
var OrganizationModel = require('./organization-model');
var GroupModel = require('./group-model');
var ACLItemModel = require('./acl-item-model');
var READ_ONLY = 'r';
var READ_WRITE = 'rw';
/**
* manages a cartodb permission object, it contains:
* - owner: an UserModel 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
*
*/
var PermissionModel = module.exports = Backbone.Model.extend({
urlRoot: function () {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('perm');
return baseUrl + '/api/' + version + '/perm';
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
this.acl = new Backbone.Collection();
this.owner = opts._userModel;
this._generateOwner();
this._generateAcl();
this._initBinds();
},
_initBinds: function () {
this.bind('change:owner', this._generateOwner, this);
this.bind('change:acl', this._generateAcl, this);
},
_generateOwner: function () {
if (!this.owner) {
this.owner = new UserModel(null, {
configModel: this._configModel
});
}
this.owner.set(this.get('owner'));
},
getOwner: function () {
return this.owner;
},
_generateAcl: function () {
var self = this;
this.acl.reset([], { silent: true });
_.each(this.get('acl'), function (aclItem) {
var model;
switch (aclItem.type) {
case 'user':
model = new UserModel(aclItem.entity, {
configModel: self._configModel
});
break;
case 'org':
model = new OrganizationModel(aclItem.entity, {
configModel: self._configModel
});
break;
case 'group':
model = new GroupModel(aclItem.entity, {
configModel: self._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 = Utils.result(this.findRepresentableAclItem(model), 'get', 'access');
return access === READ_WRITE;
},
canChangeReadAccess: function (model) {
return this._canChangeAccess(model);
},
canChangeWriteAccess: function (model) {
return (!model.isBuilder || model.isBuilder()) && this._canChangeAccess(model, function (representableAclItem) {
return Utils.result(representableAclItem, 'get', 'access') !== READ_WRITE;
});
},
_canChangeAccess: function (model) {
var representableAclItem = this.findRepresentableAclItem(model);
return this.isOwner(model) || !representableAclItem ||
representableAclItem === this._ownAclItem(model) || Utils.result(arguments, 1, representableAclItem) || false;
},
grantWriteAccess: function (model) {
this._grantAccess(model, READ_WRITE);
},
grantReadAccess: function (model) {
this._grantAccess(model, 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);
}
},
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, 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.log('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) {
// try to get the organization from the user groups collection,
// then from the user model (Editor),
// and finally from the user model (Builder)
var org = _.result(m.collection, 'organization') || m.organization || m._organizationModel;
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 && Utils.result(aclItem, 'get', 'access') !== 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) {
var type;
if (model instanceof UserModel) {
type = 'user';
} else if (model instanceof GroupModel) {
type = 'group';
} else if (this._isOrganization(model)) {
type = 'org';
} else {
throw new Error('model not recognized as a valid ACL entity ' + model);
}
return new ACLItemModel({
type: type,
entity: model,
access: access
});
}
});
exports.READ_ONLY = READ_ONLY;
exports.READ_WRITE = READ_WRITE;

View File

@@ -0,0 +1,117 @@
var _ = require('underscore');
var Backbone = require('backbone');
var syncAbort = require('./backbone/sync-abort');
var STATUS = require('./query-base-status');
var MAX_GET_LENGTH = 1024;
var MAX_REPEATED_ERRORS = 3;
module.exports = Backbone.Model.extend({
initialize: function (attrs, options) {
this.repeatedErrors = 0;
this.listenTo(this, 'change:status', this._onStatusChanged);
this.listenTo(this, 'change:ready', this._onReadyChanged);
},
/**
* @override {Backbone.prototype.sync} abort ongoing request if there is any
*/
sync: syncAbort,
getStatusValue: function () {
return this.get('status');
},
isInInitialStatus: function () {
return this.get('status') === STATUS.initial;
},
isFetched: function () {
return this.get('status') === STATUS.fetched;
},
isFetching: function () {
return this.get('status') === STATUS.fetching;
},
isErrored: function () {
return this.get('status') === STATUS.errored;
},
isUnavailable: function () {
return this.get('status') === STATUS.unavailable;
},
isDone: function () {
return this.isFetched() || this.isErrored();
},
isInFinalStatus: function () {
var finalStatuses = [STATUS.unavailable, STATUS.fetched, STATUS.errored];
return _.contains(finalStatuses, this.get('status'));
},
canFetch: function () {
var hasQuery = this.hasQuery();
var isReady = this.get('ready');
return hasQuery && isReady;
},
hasQuery: function () {
return !!this.get('query');
},
shouldFetch: function () {
return this.canFetch() && !this.isFetched() && !this.isFetching() && !this.isErrored();
},
resetFetch: function () {
this.set('status', STATUS.unfetched);
},
/**
* @override {Backbone.prototype.isNew} for this.destroy() to work (not try to send DELETE request)
*/
isNew: function () {
return true;
},
hasRepeatedErrors: function () {
return this.repeatedErrors >= MAX_REPEATED_ERRORS;
},
_onStatusChanged: function () {
if (this.isInFinalStatus()) {
this.trigger('inFinalStatus');
}
},
_addChangeListener: function () {
this.bind('change', this._onChange, this);
},
_removeChangeListener: function () {
this.unbind('change', this._onChange, this);
},
_httpMethod: function () {
return this._getSqlApiQueryParam().length > MAX_GET_LENGTH
? 'POST'
: 'GET';
},
_incrementRepeatedError: function () {
this.repeatedErrors++;
},
_resetRepeatedError: function () {
this.repeatedErrors = 0;
},
_onReadyChanged: function () {
if (this.get('ready') && this.shouldFetch()) {
this.fetch();
}
}
});

View File

@@ -0,0 +1,8 @@
module.exports = {
initial: 'initial',
unavailable: 'unavailable',
unfetched: 'unfetched',
fetching: 'fetching',
fetched: 'fetched',
errored: 'errored'
};

View File

@@ -0,0 +1,101 @@
var _ = require('underscore');
var Backbone = require('backbone');
var DEACTIVATE_MATRIX = {
'number': ['date'],
'boolean': ['date'],
'date': ['boolean']
};
var DESTRUCTIVE_MATRIX = {
'string': {
'string': false,
'number': true,
'date': true,
'boolean': true
},
'number': {
'string': false,
'number': false,
'date': true,
'boolean': true
},
'date': {
'string': false,
'number': true,
'date': false,
'boolean': true
},
'boolean': {
'string': false,
'number': false,
'date': true,
'boolean': false
}
};
var NON_EDITABLE_ATTRIBUTES = [
'the_geom_webmercator',
'cartodb_id',
'the_geom'
];
module.exports = Backbone.Model.extend({
url: function () {
if (this._tableName) {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('column');
var url = baseUrl + '/api/' + version + '/tables/' + this._tableName + '/columns/';
if (!this.isNew()) {
// If name changes, we should point to the "old" name
// in order to make the correct call
url += this.previous('name') || this.get('name');
}
return url;
}
return false;
},
parse: function (attrs) {
return {
name: attrs.name,
type: attrs.cartodb_type || attrs.type,
isNew: false
};
},
initialize: function (models, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._tableName = opts.tableName;
this._configModel = opts.configModel;
},
isNew: function () {
return this.get('isNew');
},
isEditable: function () {
return !_.contains(NON_EDITABLE_ATTRIBUTES, this.get('name'));
},
// This is relative becuase we could set any value to cartodb_id
isCartoDBIDColumn: function () {
return this.get('name') === 'cartodb_id' && this.get('type') === 'number';
},
isGeometryColumn: function () {
return this.get('type') === 'geometry';
}
}, {
isTypeChangeDestructive: function (type, newType) {
return DESTRUCTIVE_MATRIX[type][newType];
},
isTypeChangeAllowed: function (type, newType) {
var deactivated = DEACTIVATE_MATRIX[type] || [];
deactivated = deactivated.concat([type]);
return !_.contains(deactivated, newType);
}
});

View File

@@ -0,0 +1,78 @@
var Backbone = require('backbone');
var _ = require('underscore');
var QueryColumnModel = require('./query-column-model');
module.exports = Backbone.Collection.extend({
model: function (attrs, opts) {
var self = opts.collection;
return new QueryColumnModel(attrs, {
configModel: self._configModel,
tableName: self._tableName
});
},
url: function () {
if (this._tableName) {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('column');
return baseUrl + '/api/' + version + '/tables/' + this._tableName + '/columns';
}
return false;
},
initialize: function (models, opts) {
if (!opts.configModel) throw new Error('configModel is required');
if (!opts.querySchemaModel) throw new Error('querySchemaModel is required');
this._tableName = opts.tableName;
this._querySchemaModel = opts.querySchemaModel;
this._configModel = opts.configModel;
this._initBinds();
},
_initBinds: function () {
this.bind('add remove change:type change:name', function () {
this.reset();
this._querySchemaModel.set('status', 'unfetched');
this._querySchemaModel.fetch();
}, this);
this._querySchemaModel.bind('change:status', function (mdl, status) {
if (status === 'fetched') {
this.reset(this._querySchemaModel.columnsCollection.toJSON());
}
}, this);
},
setTableName: function (name) {
if (!name) return;
if (this._tableName) {
this._tableName = name;
this.each(function (columnModel) {
columnModel._tableName = name;
});
}
},
addColumn: function (opts) {
opts = opts || {};
this.create(
{
name: 'column_' + new Date().getTime(),
type: 'string',
isNew: true
},
_.extend(
opts,
{
wait: true,
parse: false
}
)
);
}
});

View File

@@ -0,0 +1,165 @@
var _ = require('underscore');
var Backbone = require('backbone');
var BaseModel = require('./query-base-model');
var STATUS = require('./query-base-status');
/* eslint-disable */
var template = _.template("" +
"SELECT CASE LOWER(ST_GeometryType(<%= geom_column %>)) " +
" WHEN 'st_polygon' THEN 'polygon' " +
" WHEN 'st_multipolygon' THEN 'polygon' " +
" WHEN 'st_multilinestring' THEN 'line' " +
" WHEN 'st_linestring' THEN 'line' " +
" WHEN 'st_multipoint' THEN 'point' " +
" WHEN 'st_point' THEN 'point' " +
" ELSE '' " +
" END AS the_geom FROM (<%= sql %>) __wrapped WHERE <%= geom_column %> IS NOT NULL"
/* eslint-enable */
);
var THE_GEOM_NOT_FOUND_ERROR = 'column \"the_geom\" does not exist';
var PARAMS = {
sort_order: 'asc',
rows_per_page: 40,
page: 0
};
var GEOMETRIES = {
LINE: 'line',
POINT: 'point',
POLYGON: 'polygon'
};
/**
* Model to represent a schema of a SQL query.
*/
module.exports = BaseModel.extend({
defaults: {
query: '',
status: STATUS.initial,
simple_geom: '', // , 'point', 'polygon', 'line'
ready: false // until true there's no data available on the table(s) used in the query
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
BaseModel.prototype.initialize.call(this, attrs, opts);
this._configModel = opts.configModel;
this._addChangeListener();
},
url: function () {
return this._configModel.getSqlApiUrl();
},
fetch: function (opts) {
if (!this.canFetch()) return;
this.set('status', STATUS.fetching);
opts = opts || {};
opts.errorCallback = opts && (opts.originalError || opts.error);
var successCallback = opts && (opts.success || opts.complete);
opts.data = _.extend(
opts.data || {},
{
api_key: this._configModel.get('api_key'),
q: this._getSqlApiQueryParam(opts.geomColumnName)
},
PARAMS
);
opts.method = this._httpMethod();
opts.success = function (model, response) {
this._resetRepeatedError();
if (successCallback) { successCallback(response); }
}.bind(this);
opts.error = function (model, response) {
if (response && response.statusText !== 'abort') {
var error = response.responseText ? JSON.parse(response.responseText).error : [];
// in case we get an error of the_geom does not exists try with the_geom_webmercator to get the geometry type
if (error.length === 1 && error[0] === THE_GEOM_NOT_FOUND_ERROR) {
this.fetch(_.extend({}, _.omit(opts, 'success'), { originalError: opts.errorCallback, geomColumnName: 'the_geom_webmercator' }));
} else {
this._incrementRepeatedError();
this.set({
simple_geom: '',
query_errors: error,
status: this.hasRepeatedErrors() ? STATUS.errored : STATUS.unavailable
});
if (opts.errorCallback) {
opts.errorCallback({
status: response.status || 'Unknown',
error: error
});
}
}
}
}.bind(this);
return Backbone.Model.prototype.fetch.call(this, opts);
},
parse: function (r) {
var simpleGeom;
_.some(r.rows, function (row) {
return !!(simpleGeom = row.the_geom); // to stop when found a valid simple geom
});
return {
status: STATUS.fetched,
simple_geom: simpleGeom || ''
};
},
hasValue: function () {
throw new Error('QueryGeometryModel.hasValue() is an async operation. Use `.hasValueAsync` instead.');
},
hasValueAsync: function () {
var self = this;
if (this.isFetched()) {
return Promise.resolve(!!this.get('simple_geom'));
} else {
return new Promise(function (resolve) {
self.listenToOnce(self, 'inFinalStatus', function () {
resolve(!!self.get('simple_geom'));
});
});
}
},
_onChange: function () {
this._removeChangeListener();
if (!this.hasChanged('status') && this.get('status') === STATUS.fetching) {
// If it is already fetching just redo the fetch with latest attrs
// in case the query has changed
if (this.hasChanged('query')) {
this.fetch();
}
} else if (this.hasChanged('query') || this.hasChanged('ready')) {
this.set('status', this.get('query') ? STATUS.unfetched : STATUS.unavailable, { silent: true });
}
this._addChangeListener();
},
_getSqlApiQueryParam: function (geomColumnName) {
return template({
geom_column: geomColumnName || 'the_geom',
sql: this.get('query')
});
},
isPolygon: function () {
return this.get('simple_geom') === GEOMETRIES.POLYGON;
}
});

View File

@@ -0,0 +1,111 @@
var Backbone = require('backbone');
var _ = require('underscore');
module.exports = Backbone.Model.extend({
// In case that there is already an id attribute
idAttribute: '__id',
url: function () {
if (this._tableName && this._configModel) {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('record');
var url = baseUrl + '/api/' + version + '/tables/';
if (this.hasWriteAccess(this._userModel) && !this.isOwner(this._userModel)) {
var owner = this._permissionModel.getOwner();
var ownerUsername = owner.get('username');
url += ownerUsername + '.' + this._tableName + '/records';
} else {
url += this._tableName + '/records';
}
if (!this.isNew()) {
url += '/' + this.get('cartodb_id');
}
return url;
}
return false;
},
initialize: function (attrs, opts) {
this.collection = this.collection || {};
this._tableName = opts.tableName || this.collection._tableName;
this._configModel = opts.configModel || this.collection._configModel;
this._permissionModel = opts.permissionModel || this.collection._permissionModel;
this._userModel = opts.userModel || this.collection._userModel;
this._initBinds();
},
_initBinds: function () {
this.bind('error', this._onError, this);
},
hasWriteAccess: function (userModel) {
if (!userModel || !this._permissionModel) {
return false;
}
return this._permissionModel.hasWriteAccess(userModel);
},
isOwner: function (userModel) {
if (!userModel || !this._permissionModel) {
return false;
}
return this._permissionModel.isOwner(userModel);
},
isNew: function () {
return !this.has('cartodb_id');
},
isGeomLoaded: function () {
try {
JSON.parse(this.get('the_geom'));
return true;
} catch (e) {
return false;
}
},
fetchRowIfGeomIsNotLoaded: function (callback) {
if (this.isGeomLoaded()) {
callback();
return;
}
this.fetch({
success: callback
});
},
_onError: function () {
this.set(this.previousAttributes());
},
sync: function () {
this.trigger('loading', this);
Backbone.Model.prototype.sync.apply(this, arguments);
},
parse: function (attrs) {
if (!attrs.__id) {
attrs.__id = _.uniqueId();
}
return attrs;
},
toJSON: function () {
return _.omit(
this.attributes,
'__id',
'the_geom_webmercator'
);
}
});

View File

@@ -0,0 +1,308 @@
var Backbone = require('backbone');
var QueryRowModel = require('./query-row-model');
var syncAbort = require('./backbone/sync-abort');
var _ = require('underscore');
var STATUS = require('./query-base-status');
var MAX_GET_LENGTH = 1024;
var WRAP_SQL_TEMPLATE = 'select <%= selectedColumns %> from (<%= sql %>) __wrapped';
var MAX_REPEATED_ERRORS = 3;
module.exports = Backbone.Collection.extend({
defaults: {
empty: true
},
DEFAULT_FETCH_OPTIONS: {
rows_per_page: 40,
sort_order: 'asc',
page: 0
},
// Due to a problem how Backbone checks if there is a duplicated model
// or not, we can't create the model with a function + its necessary options
model: QueryRowModel,
sync: syncAbort,
url: function () {
return this._configModel.getSqlApiUrl();
},
initialize: function (models, opts) {
opts = opts || {};
this._repeatedErrors = 0;
this._tableName = opts.tableName;
this._querySchemaModel = opts.querySchemaModel;
this._configModel = opts.configModel;
this.statusModel = new Backbone.Model({
status: STATUS.initial
});
this._initBinds();
},
_initBinds: function () {
this.listenTo(this._querySchemaModel, 'change:query', this._onQuerySchemaQueryChange);
if (this._querySchemaModel && this._querySchemaModel.columnsCollection) {
this.listenTo(this._querySchemaModel.columnsCollection, 'reset', this._onColumnsCollectionReset);
}
this.listenTo(this.statusModel, 'change:status', this._onStatusChanged);
},
_onColumnsCollectionReset: function () {
this._onQuerySchemaQueryChange();
if (this.shouldFetch()) {
this.fetch();
}
},
getStatusValue: function () {
return this.statusModel.get('status');
},
isInInitialStatus: function () {
return this.getStatusValue() === STATUS.initial;
},
isFetched: function () {
return this.getStatusValue() === STATUS.fetched;
},
isErrored: function () {
return this.getStatusValue() === STATUS.errored;
},
isFetching: function () {
return this.getStatusValue() === STATUS.fetching;
},
isDone: function () {
return this.isFetched() || this.isErrored();
},
isInFinalStatus: function () {
var finalStatuses = [STATUS.unavailable, STATUS.fetched, STATUS.errored];
return _.contains(finalStatuses, this.getStatusValue());
},
isUnavailable: function () {
return this.getStatusValue() === STATUS.unavailable;
},
canFetch: function () {
var hasQuery = !!this._querySchemaModel.get('query');
var isReady = this._querySchemaModel.get('ready');
var isFetched = this._querySchemaModel.isFetched();
var isErrored = this._querySchemaModel.isErrored();
return hasQuery && isReady && isFetched && !isErrored;
},
shouldFetch: function () {
return !this.isFetched() && !this.isFetching() && this.canFetch() && !this.isErrored();
},
resetFetch: function () {
this.statusModel.set('status', STATUS.unfetched);
},
isEmpty: function () {
throw new Error('QueryRowsCollection.isEmpty() is an async operation. Use `.isEmptyAsync` instead.');
},
isEmptyAsync: function () {
if (this.isInFinalStatus()) {
return Promise.resolve(this.size() === 0);
} else {
return new Promise(function (resolve) {
this.listenToOnce(this, 'inFinalStatus', function () {
resolve(this.size() === 0);
});
}.bind(this));
}
},
_onStatusChanged: function () {
if (this.isInFinalStatus()) {
this.trigger('inFinalStatus');
}
},
_onQuerySchemaQueryChange: function () {
this.statusModel.set('status', 'unfetched');
this.reset([], { silent: true });
},
_geometryColumnSQL: function (column) {
/* eslint-disable */
return [
"CASE",
"WHEN GeometryType(" + column + ") = 'POINT' THEN",
"ST_AsGeoJSON(" + column + ",8)",
"WHEN (" + column + " IS NULL) THEN",
"NULL",
"ELSE",
"GeometryType(" + column + ")",
"END " + column
].join(' ');
/* eslint-enable */
},
// 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
_getWrappedSQL: function (excludeColumns) {
var self = this;
var schema = this._querySchemaModel.columnsCollection.toJSON();
var selectedColumns = _
.chain(schema)
.omit(function (item) {
return _.contains(excludeColumns, item.name);
})
.map(function (item, index) {
if (item.type === 'geometry') {
return self._geometryColumnSQL(item.name);
}
return '"' + item.name + '"';
})
.value()
.join(',');
return _.template(WRAP_SQL_TEMPLATE)({
selectedColumns: selectedColumns,
sql: this._querySchemaModel.get('query')
});
},
_httpMethod: function () {
return this._querySchemaModel.get('query').length > MAX_GET_LENGTH
? 'POST'
: 'GET';
},
_incrementRepeatedError: function () {
this._repeatedErrors++;
},
_resetRepeatedError: function () {
this._repeatedErrors = 0;
},
hasRepeatedErrors: function () {
return this._repeatedErrors >= MAX_REPEATED_ERRORS;
},
fetch: function (opts) {
opts = opts || {};
var previousStatus = this.getStatusValue();
this.statusModel.set('status', STATUS.fetching);
var excludeColumns = (opts.data && opts.data.exclude) || [];
opts = opts || {};
opts.data = _.extend(
{},
this.DEFAULT_FETCH_OPTIONS,
opts.data && _.omit(opts.data, 'exclude') || {},
{
api_key: this._configModel.get('api_key'),
q: this._getWrappedSQL(excludeColumns)
}
);
opts.method = this._httpMethod();
var errorCallback = opts.error;
opts.error = function (coll, resp) {
if (previousStatus === STATUS.unavailable) {
this._incrementRepeatedError();
}
if (resp && resp.error) {
this._querySchemaModel.setError(resp.error);
}
this.trigger('fail', coll, resp);
this.statusModel.set('status', this.hasRepeatedErrors() ? STATUS.errored : STATUS.unavailable);
errorCallback && errorCallback(coll, resp);
}.bind(this);
var successCallback = opts.success;
opts.success = function (coll, resp, options) {
if (resp && resp.error) {
return opts.error.apply(this, arguments);
}
this._resetRepeatedError();
this.statusModel.set('status', STATUS.fetched);
successCallback && successCallback(coll, resp);
}.bind(this);
// Needed to reset the whole collection when a fetch is done
opts.reset = true;
return Backbone.Collection.prototype.fetch.call(this, opts);
},
parse: function (r) {
return this._parseWithID(r.rows);
},
reset: function (result, opts) {
var items = [];
if (result && result.rows) {
// If reset comes from a fetch, we need to parse the rows
items = result.rows;
} else {
// If it comes directly from a simple reset function
items = result;
}
Backbone.Collection.prototype.reset.apply(this, [this._parseWithID(items)]);
},
_parseWithID: function (array) {
return _.map(array, function (attrs) {
attrs.__id = _.uniqueId();
return attrs;
});
},
addRow: function (opts) {
opts = opts || {};
this.create(
{
__id: _.uniqueId()
},
_.extend(
opts,
{
wait: true,
parse: true
}
)
);
},
setTableName: function (name) {
if (!name) return;
if (this._tableName) {
this._tableName = name;
this.each(function (rowModel) {
rowModel._tableName = name;
});
}
}
});

View File

@@ -0,0 +1,175 @@
var _ = require('underscore');
var Backbone = require('backbone');
var BaseModel = require('./query-base-model');
var QueryRowsCollection = require('./query-rows-collection');
var SQLUtils = require('builder/helpers/sql-utils');
var STATUS = require('./query-base-status');
var DEFAULT_TABLE_QUERY_TEMPLATE = _.template('SELECT * FROM <%= tableName %>');
var WRAPPED_SQL_QUERY_TEMPLATE = _.template('SELECT * FROM (<%= sql %>) __wrapped');
/**
* Model to represent a schema of a SQL query.
*/
module.exports = BaseModel.extend({
defaults: {
query: '',
sort_order: 'asc',
rows_per_page: 0,
page: 0,
status: STATUS.initial,
ready: false // until true there's no data available on the table(s) used in the query
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
BaseModel.prototype.initialize.call(this, attrs, opts);
this._configModel = opts.configModel;
this.columnsCollection = new Backbone.Collection([]);
this.rowsCollection = new QueryRowsCollection([]);
if (this.get('status') === STATUS.initial) {
this._setStatusPerQueryValue();
}
this._addChangeListener();
},
url: function () {
return this._configModel.getSqlApiUrl();
},
fetch: function (opts) {
if (!this.canFetch()) return;
this.set('status', STATUS.fetching);
opts = opts || {};
opts.errorCallback = opts && (opts.originalError || opts.error);
var successCallback = opts && (opts.success || opts.complete);
opts.data = _.extend(
opts.data || {},
{
sort_order: this.get('sort_order'),
rows_per_page: this.get('rows_per_page'),
page: this.get('page'),
api_key: this._configModel.get('api_key'),
q: this._getSqlApiQueryParam()
}
);
opts.method = this._httpMethod();
opts.success = function (model, response) {
this._resetRepeatedError();
successCallback && successCallback(response);
}.bind(this);
opts.error = function (model, response) {
if (response && response.statusText !== 'abort') {
var error = response.responseText ? JSON.parse(response.responseText).error : [];
this._incrementRepeatedError();
this.set({
query_errors: error,
status: this.hasRepeatedErrors() ? STATUS.errored : STATUS.unavailable
});
opts.errorCallback && opts.errorCallback({
status: response.status || 'Unknown',
error: error
});
}
}.bind(this);
return Backbone.Model.prototype.fetch.call(this, opts);
},
parse: function (r) {
this.rowsCollection.reset(r.rows);
this.columnsCollection.reset(
_.map(r.fields, function (d, name) {
return {
name: name,
type: d.type
};
})
);
return { status: STATUS.fetched };
},
hasDefaultQueryFor: function (tableName) {
if (!this.hasQuery()) {
return false;
}
var defaultQueryForTableName = DEFAULT_TABLE_QUERY_TEMPLATE({ tableName: tableName });
return SQLUtils.isSameQuery(this.get('query'), defaultQueryForTableName);
},
getColumnNames: function () {
return this.columnsCollection.pluck('name');
},
getColumnType: function (columnName) {
return this.isFetched() ? this.columnsCollection.findWhere({ name: columnName }).get('type') : undefined;
},
_onChange: function () {
this._removeChangeListener();
if (!this.hasChanged('status') && this.get('status') === STATUS.fetching) {
this.fetch(); // If is already fetching just redo the fetch with latest attrs
} else {
if (this.hasChanged('query')) {
this.columnsCollection.reset();
this._setStatusPerQueryValue();
}
}
this._addChangeListener();
},
_setStatusPerQueryValue: function () {
this.set('status', this.get('query') ? STATUS.unfetched : STATUS.unavailable, { silent: true });
},
setError: function (error) {
var queryErrors = this.get('query_errors');
if (queryErrors && queryErrors.length) {
return;
}
this._incrementRepeatedError();
this.set({
query_errors: error,
status: this.hasRepeatedErrors() ? STATUS.errored : STATUS.unavailable
});
},
// Basically checks if an schema is different than the current one
hasDifferentSchemaThan: function (schemaArray) {
return !_.every(schemaArray, function (columnObj) {
var columnModel = this.columnsCollection.findWhere({ name: columnObj.name });
return columnModel && (columnModel.get('type') === columnObj.type || columnObj.type == null);
}, this);
},
resetDueToAlteredData: function () {
this.set('status', STATUS.unfetched);
this.trigger('resetDueToAlteredData');
this.fetch();
},
_getSqlApiQueryParam: function () {
return WRAPPED_SQL_QUERY_TEMPLATE({
sql: this.get('query')
});
}
});

View File

@@ -0,0 +1,53 @@
var Backbone = require('backbone');
var syncAbort = require('./backbone/sync-abort');
/**
* State definition model.
* Used to persist the dashboard state at any time
*/
module.exports = Backbone.Model.extend({
/**
* @override {Backbone.prototype.sync} abort ongoing request if there is any
*/
sync: syncAbort,
url: function () {
return this._visDefinitionModel.stateURL();
},
isNew: function () {
return false;
},
initialize: function (attrs, opts) {
if (!attrs.json) throw new Error('state json is required');
if (!opts.visDefinitionModel) throw new Error('visDefinitionModel is required');
this._visDefinitionModel = opts.visDefinitionModel;
},
toJSON: function () {
return {
json: this.get('json')
};
},
updateState: function (state) {
this.save({
json: state
});
},
setBounds: function (bounds) {
this.trigger('boundsSet', bounds);
},
getZoom: function () {
return this.get('json').map.zoom;
},
getCenter: function () {
return this.get('json').map.center;
}
});

View File

@@ -0,0 +1,40 @@
var _ = require('underscore');
var AssetModel = require('./asset-model');
module.exports = AssetModel.extend({
defaults: {
ext: 'svg',
folder: 'maki-icons',
host: 'https://s3.amazonaws.com',
bucket: 'com.cartodb.users-assets.production',
kind: 'marker',
name: '',
public_url: '',
size: '18',
state: 'idle'
},
getURLFor: function (name) {
var url = this.get('host') + '/' + this.get('bucket') + '/' + this.get('folder') + '/' + name;
var size = this.get('size') ? '-' + this.get('size') : '';
return url + size + '.' + this.get('ext');
},
toJSON: function () {
var attributes = _.clone(this.attributes);
attributes['public_url'] = this.getURLFor(attributes['icon']);
return attributes;
},
get: function (attributeName) {
var attribute = this.attributes[attributeName];
if (attributeName === 'public_url') {
attribute = this.getURLFor(this.attributes['icon']);
}
return attribute;
}
});

View File

@@ -0,0 +1,18 @@
var StaticAssetModel = require('./static-asset-model');
var AssetsCollection = require('./assets-collection');
module.exports = AssetsCollection.extend({
model: StaticAssetModel,
url: function () {
return '';
},
initialize: function (models, opts) {
},
parse: function (resp, xhr) {
return [];
}
});

View File

@@ -0,0 +1,163 @@
var Backbone = require('backbone');
var _ = require('underscore');
var $ = require('jquery');
var MULTIPLIER = 1.2; // Multiply current interval for this number
var INTERVAL = 1500; // Interval time between poll checkings
var SYNC_GAP = 900000; // Gap (in ms) necessary to perform next synchronization
var POLLING_GAP = 15000; // Gap (in seconds) necessary to start polling and checking synchronization
/**
* Synced table model
*/
module.exports = Backbone.Model.extend({
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
},
urlRoot: function () {
var version = this._configModel.urlVersion('synchronizations');
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/' + version + '/synchronizations/';
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
this.bind('destroy', function () {
this.unset('id');
});
this._checkState();
this.bind('change:error_code change:error_message change:state', this._checkState, this);
},
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;
},
_checkState: function () {
if (this.get('error_code') || this.get('error_message')) {
this.set('state', 'failure');
}
},
syncNow: function (callback) {
if (!callback) throw new Error('callback is required');
$.ajax({
url: this.url() + '/sync_now',
type: 'PUT'
}).always(callback);
},
// Checks for poll to finish
pollCheck: function (i) {
var self = this;
var interval = 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 * MULTIPLIER;
self.pollTimer = setInterval(request, interval);
}
},
destroyCheck: function () {
clearInterval(this.pollTimer);
},
isSync: function () {
return !this.isNew() && this.get('interval') > 0;
},
isSyncing: function () {
return this.get('state') === 'syncing' || this.get('state') === 'queued';
},
canSyncNow: function () {
var ranAt = new Date(this.get('ran_at'));
var now = new Date();
var gap = SYNC_GAP; // Importer needs some time to perform the next sync, set 15 min as default.
if (this.isSyncing()) {
return false;
}
return (now.getTime() - ranAt.getTime()) > gap;
},
linkToTable: function (tableModel) {
var self = this;
if (tableModel.has('synchronization')) {
this.set(tableModel.get('synchronization'));
}
tableModel.bind('change:synchronization', function () {
self.set(tableModel.get('synchronization'));
}, tableModel);
tableModel.bind('destroy', function destroy () {
self.unbind(null, null, tableModel);
self.destroy();
}, tableModel);
}
}, {
SYNC_GAP: SYNC_GAP,
POLLING_GAP: POLLING_GAP
});

View File

@@ -0,0 +1,25 @@
var Backbone = require('backbone');
var ColumnModel = require('./column-model');
/**
* A collection of table columns
*/
module.exports = Backbone.Collection.extend({
model: ColumnModel,
initialize: function (models, opts) {
if (!opts.configModel) throw new Error('configModel is required');
if (!opts.tableModel) throw new Error('tableModel is required');
this._configModel = opts.configModel;
this._tableModel = opts.tableModel;
},
url: function () {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('column');
return baseUrl + '/api/' + version + '/tables/' + this._tableModel.get('name') + '/columns';
}
});

View File

@@ -0,0 +1,171 @@
var _ = require('underscore');
var Backbone = require('backbone');
var TableColumnsCollection = require('./table-columns-collection');
var getSimpleGeometryType = require('./get-simple-geometry-type');
var PermissionModel = require('./permission-model');
var SyncModel = require('./synchronization-model');
var TableNameUtils = require('builder/helpers/table-name-utils');
/**
* Model representing a table.
*/
module.exports = Backbone.Model.extend({
idAttribute: 'name',
defaults: {
// always not fetched to begin with, since the only way to currently get table data is by an individual request
fetched: false,
schema: []
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
},
urlRoot: function () {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('table');
return baseUrl + '/api/' + version + '/tables';
},
parse: function (r, opts) {
var configModel = (opts && opts.configModel) || this._configModel;
// "Sometimes" table presenter returns the needed data within table_visualization
// attribute, because it returns the canonical visualization data. So we have to take
// the data from there.
if (r.table_visualization && !_.isEmpty(r.table_visualization)) {
r = _.extend(
{},
r.table_visualization,
{
geometry_types: r.geometry_types
}
);
}
if (r.synchronization) {
if (!this._syncModel) {
this._syncModel = new SyncModel(r.synchronization, {
configModel: configModel
});
} else {
this._syncModel.set(r.synchronization);
}
}
if (r.permission) {
if (!this._permissionModel) {
this._permissionModel = new PermissionModel(r.permission, {
configModel: configModel
});
} else {
this._permissionModel.set(r.permission);
}
}
var attrs = _.defaults({
fetched: true
}, r);
var columnsAttrs = _.map(r.schema, function (d) {
return {
name: d[0],
type: d[1]
};
}, this);
if (!this.columnsCollection) {
this.columnsCollection = new TableColumnsCollection(columnsAttrs, {
configModel: configModel,
tableModel: this
});
} else {
this.columnsCollection.reset(columnsAttrs);
}
return attrs;
},
isOwner: function (userModel) {
if (!userModel || !this._permissionModel) {
return false;
}
return this._permissionModel.isOwner(userModel);
},
getUnqualifiedName: function () {
return TableNameUtils.getUnqualifiedName(this.get('name'));
},
getOwnerName: function () {
var name = this.get('name');
if (!name) return null;
var tk = name.split('.');
if (tk.length === 2) {
return tk[0].replace(/"/g, '');
} else if (this._permissionModel) {
var ownerModel = this._permissionModel.getOwner();
return ownerModel.get('username');
} else {
return '';
}
},
// "user".table -> user.table
getUnquotedName: function () {
var name = this.get('name');
return name && name.replace(/"/g, '');
},
getGeometryType: function () {
var types = this.get('geometry_types');
var geomTypes = [];
if (!_.isArray(types)) {
return [];
}
for (var t in types) {
var type = types[t];
// when there are rows with no geo type null is returned as geotype
if (type) {
var a = getSimpleGeometryType(type.toLowerCase());
if (a) {
geomTypes.push(a);
}
}
}
return _.uniq(geomTypes);
},
getPermissionModel: function () {
return this._permissionModel;
},
isReadOnly: function (userModel) {
return this.isSync() || !this.hasWriteAccess(userModel);
},
isSync: function () {
var syncModel = this.getSyncModel();
var isSync = syncModel && syncModel.get('id');
return !!isSync;
},
hasWriteAccess: function (userModel) {
if (!userModel || !this._permissionModel) {
return false;
}
return this._permissionModel.hasWriteAccess(userModel);
},
getSyncModel: function () {
return this._syncModel;
}
});

View File

@@ -0,0 +1,87 @@
var _ = require('underscore');
var Backbone = require('backbone');
var TableModel = require('./table-model');
var DEFAULT_FETCH_OPTIONS = {
type: 'table',
order: 'updated_at',
page: 1,
per_page: 20,
exclude_shared: false,
exclude_raster: true,
tags: '',
q: ''
};
/**
* A collection that holds Table models
*/
module.exports = Backbone.Collection.extend({
model: function (d, opts) {
var configModel = opts.collection._configModel;
return new TableModel(d, {
configModel: configModel,
parse: true
});
},
modelId: function (attrs) {
return attrs.name;
},
initialize: function (models, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
this._stats = {};
},
url: function () {
var version = this._configModel.urlVersion('visualization');
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/' + version + '/viz';
},
// Overrides the default fetch, to use the internal methods to construct parmas
fetch: function (opts) {
opts = opts || {
data: {
// If reaches this code path it's because there were no opts given, i.e. should do a 'full fetch'
// Since there is no current way to really do a full fetch let's just set a really high number to get all…
// TODO this is obviously bad for organization users, how can we do this differently
per_page: 1000
}
};
opts.data = _.extend({}, DEFAULT_FETCH_OPTIONS, opts.data);
return Backbone.Collection.prototype.fetch.call(this, opts);
},
getTotalStat: function (attribute) {
return this._stats[attribute] || 0;
},
getDefaultParam: function (param) {
return DEFAULT_FETCH_OPTIONS[param];
},
parse: function (res) {
this._stats = _.omit(res, 'visualizations');
return _.map(res.visualizations, function (d) {
var dt = d.table; // embedded table data in the vis response
return {
// From embedded table with same key/value
id: dt.id,
geometry_types: dt.geometry_types,
name: dt.name,
privacy: dt.privacy,
// From embedded table with same value, but different key
// so re-map them to match what's returned from a GET /tables/t-id
rows_counted: dt.row_count,
table_size: dt.size,
table_type: d.type // table or remote
};
}, this);
}
});

View File

@@ -0,0 +1,306 @@
module.exports = {
'international-date-line-west': {
label: _t('timezones.international-date-line-west'),
name: 'Etc/GMT+12'
},
'midway-island-samoa': {
label: _t('timezones.midway-island-samoa'),
name: 'Pacific/Midway'
},
'hawaii': {
label: _t('timezones.hawaii'),
name: 'Pacific/Honolulu'
},
'alaska': {
label: _t('timezones.alaska'),
name: 'America/Juneau'
},
'pacific-time-us-and-canada-tijuana': {
label: _t('timezones.pacific-time-us-and-canada-tijuana'),
name: 'America/Dawson'
},
'mountain-time-us-and-canada': {
label: _t('timezones.mountain-time-us-and-canada'),
name: 'America/Boise'
},
'chihuahua-la-paz-mazatlan': {
label: _t('timezones.chihuahua-la-paz-mazatlan'),
name: 'America/Chihuahua'
},
'arizona': {
label: _t('timezones.arizona'),
name: 'America/Phoenix'
},
'central-time-us-and-canada': {
label: _t('timezones.central-time-us-and-canada'),
name: 'America/Chicago'
},
'saskatchewan': {
label: _t('timezones.saskatchewan'),
name: 'America/Regina'
},
'guadalajara-mexico-city-monterrey': {
label: _t('timezones.guadalajara-mexico-city-monterrey'),
name: 'America/Mexico_City'
},
'central-america': {
label: _t('timezones.central-america'),
name: 'America/Belize'
},
'eastern-time-us-and-canada': {
label: _t('timezones.eastern-time-us-and-canada'),
name: 'America/Detroit'
},
'indiana-east': {
label: _t('timezones.indiana-east'),
name: 'America/Indiana/Indianapolis'
},
'bogota-lima-quito': {
label: _t('timezones.bogota-lima-quito'),
name: 'America/Bogota'
},
'atlantic-time-canada': {
label: _t('timezones.atlantic-time-canada'),
name: 'America/Glace_Bay'
},
'caracas-la-paz': {
label: _t('timezones.caracas-la-paz'),
name: 'America/Caracas'
},
'santiago': {
label: _t('timezones.santiago'),
name: 'America/Santiago'
},
// 'newfoundland-and-labrador': {
// label: _t('timezones.newfoundland-and-labrador'),
// name: 'America/St_Johns'
// },
'brasilia': {
label: _t('timezones.brasilia'),
name: 'America/Sao_Paulo'
},
'buenos-aires-georgetown': {
label: _t('timezones.buenos-aires-georgetown'),
name: 'America/Argentina/Buenos_Aires'
},
'greenland': {
label: _t('timezones.greenland'),
name: 'America/Godthab'
},
'mid-atlantic': {
label: _t('timezones.mid-atlantic'),
name: 'Etc/GMT+2'
},
'azores': {
label: _t('timezones.azores'),
name: 'Atlantic/Azores'
},
'cape-verde-islands': {
label: _t('timezones.cape-verde-islands'),
name: 'Atlantic/Cape_Verde'
},
'gmt-dublin-edinburgh-lisbon-london': {
label: _t('timezones.gmt-dublin-edinburgh-lisbon-london'),
name: ''
},
'casablanca-monrovia': {
label: _t('timezones.casablanca-monrovia'),
name: 'Africa/Casablanca'
},
'canary-islands': {
label: _t('timezones.canary-islands'),
name: 'Atlantic/Canary'
},
'belgrade-bratislava-budapest-ljubljana-prague': {
label: _t('timezones.belgrade-bratislava-budapest-ljubljana-prague'),
name: 'Europe/Belgrade'
},
'sarajevo-skopje-warsaw-zagreb': {
label: _t('timezones.sarajevo-skopje-warsaw-zagreb'),
name: 'Europe/Sarajevo'
},
'brussels-copenhagen-madrid-paris': {
label: _t('timezones.brussels-copenhagen-madrid-paris'),
name: 'Europe/Brussels'
},
'amsterdam-berlin-bern-rome-stockholm-vienna': {
label: _t('timezones.amsterdam-berlin-bern-rome-stockholm-vienna'),
name: 'Europe/Amsterdam'
},
'west-central-africa': {
label: _t('timezones.west-central-africa'),
name: 'Africa/Algiers'
},
'bucharest': {
label: _t('timezones.bucharest'),
name: 'Europe/Bucharest'
},
'cairo': {
label: _t('timezones.cairo'),
name: 'Africa/Cairo'
},
'helsinki-kiev-riga-sofia-tallinn-vilnius': {
label: _t('timezones.helsinki-kiev-riga-sofia-tallinn-vilnius'),
name: 'Europe/Helsinki'
},
'athens-istanbul-minsk': {
label: _t('timezones.athens-istanbul-minsk'),
name: 'Europe/Athens'
},
'jerusalem': {
label: _t('timezones.jerusalem'),
name: 'Asia/Jerusalem'
},
'harare-pretoria': {
label: _t('timezones.harare-pretoria'),
name: 'Africa/Harare'
},
'moscow-st-petersburg-volgograd': {
label: _t('timezones.moscow-st-petersburg-volgograd'),
name: 'Europe/Moscow'
},
'kuwait-riyadh': {
label: _t('timezones.kuwait-riyadh'),
name: 'Asia/Kuwait'
},
'nairobi': {
label: _t('timezones.nairobi'),
name: 'Africa/Nairobi'
},
'baghdad': {
label: _t('timezones.baghdad'),
name: 'Asia/Baghdad'
},
// 'tehran': {
// label: _t('timezones.tehran'),
// name: 'Asia/Tehran'
// },
'abu-dhabi-muscat': {
label: _t('timezones.abu-dhabi-muscat'),
name: 'Asia/Dubai'
},
'baku-tbilisi-yerevan': {
label: _t('timezones.baku-tbilisi-yerevan'),
name: 'Asia/Baku'
},
// 'kabul': {
// label: _t('timezones.kabul'),
// name: 'Asia/Kabul'
// },
'ekaterinburg': {
label: _t('timezones.ekaterinburg'),
name: 'Asia/Yekaterinburg'
},
'islamabad-karachi-tashkent': {
label: _t('timezones.islamabad-karachi-tashkent'),
name: 'Asia/Karachi'
},
// 'chennai-kolkata-mumbai-new-delhi': {
// label: _t('timezones.chennai-kolkata-mumbai-new-delhi'),
// name: 'Asia/Kolkata'
// },
// 'kathmandu': {
// label: _t('timezones.kathmandu'),
// name: 'Asia/Kathmandu'
// },
'astana-dhaka': {
label: _t('timezones.astana-dhaka'),
name: 'Asia/Dhaka'
},
// 'sri-jayawardenepura': {
// label: _t('timezones.sri-jayawardenepura'),
// name: 'Asia/Colombo'
// },
'almaty-novosibirsk': {
label: _t('timezones.almaty-novosibirsk'),
name: 'Asia/Almaty'
},
// 'yangon-rangoon': {
// label: _t('timezones.yangon-rangoon'),
// name: 'Asia/Rangoon'
// },
'bangkok-hanoi-jakarta': {
label: _t('timezones.bangkok-hanoi-jakarta'),
name: 'Asia/Bangkok'
},
'krasnoyarsk': {
label: _t('timezones.krasnoyarsk'),
name: 'Asia/Krasnoyarsk'
},
'beijing-chongqing-hong-kong-sar-urumqi': {
label: _t('timezones.beijing-chongqing-hong-kong-sar-urumqi'),
name: 'Asia/Shanghai'
},
'kuala-lumpur-singapore': {
label: _t('timezones.kuala-lumpur-singapore'),
name: 'Asia/Kuala_Lumpur'
},
'taipei': {
label: _t('timezones.taipei'),
name: 'Asia/Taipei'
},
'perth': {
label: _t('timezones.perth'),
name: 'Australia/Perth'
},
'irkutsk-ulaanbaatar': {
label: _t('timezones.irkutsk-ulaanbaatar'),
name: 'Asia/Irkutsk'
},
'seoul': {
label: _t('timezones.seoul'),
name: 'Asia/Seoul'
},
'osaka-sapporo-tokyo': {
label: _t('timezones.osaka-sapporo-tokyo'),
name: 'Asia/Tokyo'
},
'yakutsk': {
label: _t('timezones.yakutsk'),
name: 'Asia/Yakutsk'
},
// 'darwin': {
// label: _t('timezones.darwin'),
// name: 'Australia/Darwin'
// },
// 'adelaide': {
// label: _t('timezones.adelaide'),
// name: 'Australia/Adelaide'
// },
'canberra-melbourne-sydney': {
label: _t('timezones.canberra-melbourne-sydney'),
name: 'Australia/Sydney'
},
'brisbane': {
label: _t('timezones.hobart'),
name: 'Australia/Brisbane'
},
'hobart': {
label: _t('timezones.hobart'),
name: 'Australia/Hobart'
},
'vladivostok': {
label: _t('timezones.vladivostok'),
name: 'Asia/Vladivostok'
},
'guam-port-moresby': {
label: _t('timezones.guam-port-moresby'),
name: 'Pacific/Guam'
},
'magadan-solomon-islands-new-caledonia': {
label: _t('timezones.magadan-solomon-islands-new-caledonia'),
name: 'Asia/Magadan'
},
'fiji-islands-kamchatka-marshall-islands': {
label: _t('timezones.fiji-islands-kamchatka-marshall-islands'),
name: 'Pacific/Fiji'
},
'auckland-wellington': {
label: _t('timezones.auckland-wellington'),
name: 'Pacific/Auckland'
},
'nuku-alofa': {
label: _t('timezones.nuku-alofa'),
name: 'Pacific/Tongatapu'
}
};

View File

@@ -0,0 +1,91 @@
var _ = require('underscore');
var BackboneUndo = require('backbone-undo');
var MAXIMUM_STACK_LENGTH = 30;
module.exports = {
init: function (model, opts) {
if (!model) throw new Error('model is required to initialize undoManager');
opts = opts || {};
this.model = model;
this.model._undoManager = this.undoManager = new BackboneUndo({
maximumStackLength: opts.maximumStackLength || MAXIMUM_STACK_LENGTH,
register: this.model,
track: opts.track
});
this._trackEvents();
this._addMethods();
if (!_.isEmpty(opts.history)) {
opts.track && this.undoManager.stopTracking();
this._addHistory(opts.history);
opts.track && this.undoManager.startTracking();
}
},
_trackEvents: function () {
_.each(['undo', 'redo'], function (eventType) {
this.undoManager.bind(eventType, function () {
this.trigger(eventType, this.changed, this);
}, this.model);
}, this);
this.undoManager.stack.bind('add remove reset', function () {
this.trigger('unredoChanged', this.changed, this);
}, this.model);
},
_addHistory: function (history) {
var stack = this.undoManager.stack;
_.each(history, function (attrs, i) {
if (history[i + 1]) {
stack.add({
after: history[i + 1],
before: attrs,
type: 'change',
undoTypes: this.undoManager.undoTypes,
object: this.model,
magicFusionIndex: 0
});
}
}, this);
stack.pointer = stack.length - 1;
},
_addMethods: function () {
_.extend(
this.model,
{
undo: function () {
this._undoManager.undo();
},
redo: function () {
this._undoManager.redo();
},
canUndo: function () {
return this._undoManager.isAvailable('undo');
},
canRedo: function () {
return this._undoManager.isAvailable('redo');
},
getUndoHistory: function () {
var list = this._undoManager.stack.toJSON();
var data = _.reduce(list, function (memo, attrs, i) {
memo.push(attrs.before);
return memo;
}, [], this);
data.push(this.attributes);
return data;
}
}
);
}
};

View File

@@ -0,0 +1,260 @@
var _ = require('underscore');
var Backbone = require('backbone');
var UploadConfig = require('builder/config/upload-config');
var Utils = require('builder/helpers/utils.js');
var moment = require('moment');
require('backbone-model-file-upload');
/**
* Model that allows uploading files to CartoDB endpoints.
*
* NOTE: this model extends Backbone.Model instead of Backbone.Model, because
* it's required for the vendor/backbone-model-file-upload.
*/
module.exports = Backbone.Model.extend({
url: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v1/imports';
},
fileAttribute: 'filename',
defaults: {
type: '',
value: '',
interval: 0,
privacy: '',
progress: 0,
state: 'idle',
service_item_id: '',
service_name: '',
option: '',
content_guessing: true,
type_guessing: true,
create_vis: true
},
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._initBinds();
// We need to validate entry attributes
this._validate(this.attributes, { validate: true });
},
setUpload: function (d) {
this.set(d, {
validate: true
});
},
isValidToUpload: function () {
return this.get('value') && this.get('state') !== 'error';
},
setFresh: function (d) {
if (d && !_.isEmpty(d)) {
// Set upload properties except create_vis (defined when created)
this.set(_.omit(d, 'create_vis'));
} else {
this.clear();
}
},
_initBinds: function () {
this.bind('progress', function (progress) {
this.set({
progress: progress * 100,
state: 'uploading'
});
}, this);
this.bind('change:value', function () {
if (this.get('state') === 'error') {
this.set({ state: 'idle' });
this.unset('get_error_text', { silent: true });
}
}, this);
this.bind('error invalid', function (m, d) {
this.set({
state: 'error',
error_code: (d && d.error_code) || '',
get_error_text: {
title: _t('data.upload-model.invalid-import'),
what_about: (d && d.msg) || ''
}
});
}, this);
},
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')
};
}
}
},
isValid: function () {
return this.get('value') && this.get('state') !== 'error';
},
upload: function () {
if (this.get('type') === 'file') {
var self = this;
this.xhr = this.save(
{
filename: this.get('value')
},
{
success: function (m) {
m.set('state', 'uploaded');
},
error: function (m, msg) {
var message = _t('data.upload-model.connection-error');
if (msg && msg.status === 429) {
var response = JSON.parse(msg.responseText);
message = response.errors.imports;
}
self.set({
state: 'error',
get_error_text: {
title: _t('data.upload-model.error-happened'),
what_about: message
}
});
},
complete: function () {
delete self.xhr;
}
}
);
}
},
stopUpload: function () {
if (this.xhr) this.xhr.abort();
},
setGuessing: function (val) {
this.set({
type_guessing: val,
content_guessing: val
});
},
setPrivacy: function (val) {
this.set('privacy', val);
}
});

View File

@@ -0,0 +1,857 @@
var _ = require('underscore');
var $ = require('jquery');
var nodeIds = require('builder/value-objects/analysis-node-ids');
var MetricsTracker = require('builder/components/metrics/metrics-tracker');
var MetricsTypes = require('builder/components/metrics/metrics-types');
var SimpleStyleDefaults = require('builder/editor/style/style-defaults/simple-style-defaults');
var camshaftReference = require('./camshaft-reference');
var layerTypesAndKinds = require('./layer-types-and-kinds');
var layerColors = require('./layer-colors');
var Notifier = require('builder/components/notifier/notifier');
var resetStylePerNode = require('builder/helpers/reset-style-per-node');
var Router = require('builder/routes/router');
var TABLE_ORIGIN = 'table';
/**
* Coordinate side-effects done on explicit interactions.
* @param {Object} params
* @param {Object} params.userModel
* @param {Object} params.analysisDefinitionsCollection
* @param {Object} params.analysisDefinitionNodesCollection
* @param {Object} params.widgetDefinitionsCollection
* @return {Object} that contains all user-actions that the user may do related to a map
*/
module.exports = function (params) {
if (!params.userModel) throw new Error('userModel is required');
if (!params.analysisDefinitionsCollection) throw new Error('analysisDefinitionsCollection is required');
if (!params.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
if (!params.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!params.widgetDefinitionsCollection) throw new Error('widgetDefinitionsCollection is required');
var userModel = params.userModel;
var analysisDefinitionsCollection = params.analysisDefinitionsCollection;
var analysisDefinitionNodesCollection = params.analysisDefinitionNodesCollection;
var layerDefinitionsCollection = params.layerDefinitionsCollection;
var widgetDefinitionsCollection = params.widgetDefinitionsCollection;
var createDefaultCartoDBAttrs = function (oldLayerStyle) {
return {
kind: 'carto',
options: {
interactivity: '',
tile_style: (oldLayerStyle && oldLayerStyle.options.tile_style) || camshaftReference.getDefaultCartoCSSForType(),
cartocss: (oldLayerStyle && oldLayerStyle.options.tile_style) || camshaftReference.getDefaultCartoCSSForType(),
style_version: '2.1.1',
visible: true,
style_properties: (oldLayerStyle && oldLayerStyle.options.style_properties),
sql_wrap: (oldLayerStyle && oldLayerStyle.options.sql_wrap)
},
tooltip: oldLayerStyle ? oldLayerStyle.tooltip : {},
infowindow: oldLayerStyle ? oldLayerStyle.infowindow : {}
};
};
var deleteOrphanedAnalyses = function () {
analysisDefinitionNodesCollection
.toArray()
.forEach(function (nodeDefModel) {
if (!layerDefinitionsCollection.anyContainsNode(nodeDefModel)) {
nodeDefModel.destroy();
}
});
analysisDefinitionsCollection
.toArray()
.forEach(function (analysisDefModel) {
if (!analysisDefModel.getNodeDefinitionModel()) {
analysisDefModel.destroy();
}
});
};
var restoreWidgetsFromLayer = function (affectedWidgets, layerDefModel, callback, callbackOptions) {
if (!affectedWidgets || !_.isArray(affectedWidgets)) throw new Error('affectedWidgets is required');
if (!layerDefModel) throw new Error('layerDefModel is required');
if (!callback) throw new Error('callback is required');
var notification;
var restoringError;
var onWidgetFinished = _.after(affectedWidgets.length, function () {
if (notification) {
if (!restoringError) {
notification.set({
status: 'success',
info: _t('notifications.widgets.restored'),
closable: true
});
} else {
Notifier.removeNotification(notification);
}
}
callback(layerDefModel, callbackOptions);
});
if (affectedWidgets.length > 0) {
notification = Notifier.addNotification({
status: 'loading',
info: _t('notifications.widgets.restoring'),
closable: false
});
// Create widgets with new source and layer_id
_.each(affectedWidgets, function (attrs) {
attrs.layer_id = attrs.layer_id || layerDefModel.id;
widgetDefinitionsCollection.create(
attrs, {
wait: true,
success: onWidgetFinished,
error: function (e, mdl) {
notification = Notifier.addNotification({
status: 'error',
info: _t('notifications.widgets.error.title') +
_t('notifications.widgets.error.body', {
body: '',
error: mdl && mdl.get('title')
}),
closable: true
});
restoringError = true;
onWidgetFinished();
}
}
);
});
} else {
callback(layerDefModel, callbackOptions);
}
};
return {
_resetStylePerNode: function (nodeDefModel, layerDefModel, forceStyleUpdate, resetQueryReady) {
resetStylePerNode(nodeDefModel, layerDefModel, forceStyleUpdate, resetQueryReady);
},
saveAnalysis: function (analysisFormModel) {
if (!analysisFormModel.isValid()) return;
var nodeDefModel = analysisDefinitionNodesCollection.get(analysisFormModel.id);
if (nodeDefModel) {
analysisFormModel.updateNodeDefinition(nodeDefModel);
var layerDefModel = layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
MetricsTracker.track(MetricsTypes.MODIFIED_ANALYSIS, {
analysis: {
id: analysisFormModel.attributes['id'],
natural_id: analysisFormModel.attributes['id'],
type: analysisFormModel.attributes['type']
}
});
this._resetStylePerNode(nodeDefModel, layerDefModel, false, true);
} else {
nodeDefModel = analysisFormModel.createNodeDefinition(this); // delegate creation to the form, but expect it to call userActions.createAnalysisNode when done with its internal stuff
}
nodeDefModel.USER_SAVED = true;
return nodeDefModel;
},
/**
* Creates a new analysis node on a particular layer.
* It's assumed to be created on top of an existing node.
* @param {Object} nodeAttrs
* @param {Object} layerDefModel - instance of layer-definition-model
* @return {Object} instance of analysis-definition-node-model
*/
createAnalysisNode: function (nodeAttrs, layerDefModel) {
var nodeDefModel = analysisDefinitionNodesCollection.add(nodeAttrs, { parse: false });
layerDefModel.save({
cartocss: camshaftReference.getDefaultCartoCSSForType(),
source: nodeDefModel.id
});
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
MetricsTracker.track(MetricsTypes.CREATED_ANALYSIS, {
analysis: {
id: nodeDefModel.attributes['id'],
natural_id: nodeDefModel.attributes['id'],
type: nodeDefModel.attributes['type']
}
});
this._resetStylePerNode(nodeDefModel, layerDefModel);
return nodeDefModel;
},
saveAnalysisSourceQuery: function (query, nodeDefModel, layerDefModel) {
if (!nodeDefModel) throw new Error('nodeDefModel is required');
if (nodeDefModel.get('type') !== 'source') throw new Error('nodeDefModel must be a source node');
nodeDefModel.set('query', query || '');
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
this._resetStylePerNode(nodeDefModel, layerDefModel);
layerDefModel.save(); // to make sure layer's source ref is persisted
},
saveWidgetOption: function (widgetOptionModel) {
if (!widgetOptionModel) throw new Error('widgetOptionModel is required');
if (widgetOptionModel.analysisDefinitionNodeModel()) { // Might not always have a node-definition, e.g. time-series none-option
var layerDefModel = widgetOptionModel.layerDefinitionModel();
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
}
// delegate back to form model to decide how to save
return widgetOptionModel.save(widgetDefinitionsCollection);
},
goToEditWidget: function (widgets) {
if (widgets.length === 1) {
Router.goToWidget(widgets[0].id);
}
},
updateWidgetsOrder: function (widgets) {
return widgetDefinitionsCollection.updateWidgetsOrder(widgets);
},
saveWidget: function (widgetDefModel) {
if (!widgetDefModel) throw new Error('widgetDefModel is required');
var widgetLayerId = widgetDefModel.get('layer_id');
layerDefinitionsCollection.some(function (layerDefModel) {
if (layerDefModel.id === widgetLayerId) {
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
return true; // aborts the "some"-iterator
}
});
widgetDefModel.save();
},
deleteAnalysisNode: function (nodeId) {
var nodeDefModel = analysisDefinitionNodesCollection.get(nodeId);
if (!nodeDefModel) return false; // abort if there is no node-definition; nothing to delete/change
if (!nodeDefModel.canBeDeletedByUser()) return false;
var layerDefModel = layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);
var primarySourceNode = nodeDefModel.getPrimarySource();
var containsNode = function (m) {
return m !== layerDefModel && m !== nodeDefModel && m.containsNode(nodeDefModel);
};
_
.flatten([
widgetDefinitionsCollection.filter(containsNode),
layerDefinitionsCollection.filter(containsNode),
analysisDefinitionsCollection.filter(containsNode),
analysisDefinitionNodesCollection.filter(containsNode),
nodeDefModel
], true)
.forEach(function (m) {
m.destroy();
});
// Try to restore old styles if we have them, reset them if not
var oldNodeStyle = primarySourceNode.getStyleHistoryForLayer(layerDefModel.id);
if (oldNodeStyle) {
layerDefModel.set({ source: primarySourceNode.id }, { ignoreSchemaChange: true });
layerDefModel.styleModel.set(layerDefModel.styleModel.parse(oldNodeStyle.options.style_properties));
layerDefModel.set({
cartocss: oldNodeStyle.options.tile_style,
infowindow: oldNodeStyle.infowindow,
tooltip: oldNodeStyle.tooltip,
sql_wrap: oldNodeStyle.options.sql_wrap
});
} else {
layerDefModel.set({ source: primarySourceNode.id });
}
layerDefModel.save();
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
MetricsTracker.track(MetricsTypes.DELETED_ANALYSIS, {
analysis: {
id: nodeDefModel.attributes['id'],
natural_id: nodeDefModel.attributes['id'],
type: nodeDefModel.attributes['type']
}
});
this._resetStylePerNode(primarySourceNode, layerDefModel);
deleteOrphanedAnalyses();
},
/**
* Create a new layer for a given table name
* @param {model} [tableModel]
* @param {object} [options]
* @param {number} [options.at]
*/
createLayerFromTable: function (tableModel, options) {
options = options || {};
var tableName = tableModel.getUnqualifiedName();
var ownerName = tableModel.getOwnerName() || userModel.get('username');
var tableGeometry = tableModel.getGeometryType() && tableModel.getGeometryType()[0];
// Setting at assumes there to be at least one non-basemap layer
var at = options.at || layerDefinitionsCollection.length;
var layerOnTop = layerDefinitionsCollection.getLayerOnTop();
var layerOnTopPos = layerDefinitionsCollection.indexOf(layerOnTop);
var hasTorque = layerDefinitionsCollection.isThereAnyTorqueLayer();
var hasLabels = layerOnTop &&
!layerTypesAndKinds.isCartoDBType(layerOnTop.get('type')) &&
!layerTypesAndKinds.isTorqueType(layerOnTop.get('type'));
if (hasTorque || hasLabels) {
layerOnTop.set('order', layerOnTopPos + 1); // persisted on collection.save in success callback below
if (hasLabels) { at--; }
if (hasTorque) { at--; }
}
var newLetter = layerDefinitionsCollection.nextLetter();
var attrs = createDefaultCartoDBAttrs();
attrs.letter = newLetter;
attrs.options.table_name = tableName;
if (ownerName) {
attrs.options.user_name = ownerName;
}
if (tableGeometry) {
attrs.options.style_properties = {
type: 'simple',
properties: SimpleStyleDefaults.generateAttributes(tableGeometry)
};
}
analysisDefinitionNodesCollection.addRelatedTableData(tableModel.toJSON());
return layerDefinitionsCollection.create(attrs, {
wait: true,
at: at,
error: options.error,
origin: TABLE_ORIGIN,
success: function () {
layerDefinitionsCollection.each(function (layerDefModel) {
if (!layerDefModel.isDataLayer()) return;
if (analysisDefinitionsCollection.findAnalysisForLayer(layerDefModel)) return;
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
});
layerDefinitionsCollection.save();
var newLayerModel = _.last(layerDefinitionsCollection.models);
options.success && options.success(newLayerModel);
}
});
},
/**
* A layer for an existing node have different side-effects depending on the context in which the node exists.
* @param {string} nodeid
* @param {string} letter of the layer where the node change comes
* @param {object} cfg
* @param {number} cfg.at
*/
createLayerForAnalysisNode: function (nodeId, fromLayerLetter, cfg) {
var userMaxLayers = userModel.get('limits').max_layers;
if (layerDefinitionsCollection.getNumberOfDataLayers() >= userMaxLayers) {
var err = new Error('max layers reached');
err.userMaxLayers = userMaxLayers;
throw err;
}
var nodeDefModel = analysisDefinitionNodesCollection.get(nodeId);
if (!nodeDefModel) throw new Error('node with id ' + nodeId + ' does not exist');
if (!cfg) throw new Error('cfg is required');
if (isNaN(cfg.at) || cfg.at <= 0) throw new Error('cfg.at must be on top of the base layer');
var newPosition = cfg.at;
var attrs;
var tableName;
var tableNameAlias;
var ownerName;
var oldNodeStyle;
var movementType;
var newLetter = layerDefinitionsCollection.nextLetter();
var affectedWidgetAttrsBySourceChange = [];
var onNewLayerSaved = function (layer, forceResetStyles) {
this._resetStylePerNode(layer.getAnalysisDefinitionNodeModel(), layer, forceResetStyles);
layerDefinitionsCollection.save(); // to persist layers order
MetricsTracker.track(MetricsTypes.DRAGGED_NODE);
}.bind(this);
var prevLayer = layerDefinitionsCollection.findWhere({ source: nodeId, letter: fromLayerLetter });
if (prevLayer) {
if (nodeDefModel.hasPrimarySource()) {
// Node is head of a layer, e.g. given nodeId A3 it should rename prev layer (A => B), and create a new layer (A)
// where the prev layer was to take over its letter identity and its primary source (A2).
// The motivation for this is to maintain the layer's state (styles, popup etc.) which really depends on the
// last analysis output than the layer itself:
// _______ _______ ______
// | A | | A | | B | <-- note that B is really A which just got moved & had it's nodes renamed
// | | | | | |
// | {A3} | => | | | {B1} |
// | [A2] | | [A2] | | [A2] |
// | [A1] | | [A1] | | |
// | [A0] | | [A0] | | |
// |______| |______| |______|
movementType = '1';
var prevOrder = layerDefinitionsCollection.indexOf(prevLayer);
var prevLetter = fromLayerLetter;
// Change identity of prevLayer (A) so it appears as the new layer (B), including its analysis
var renamedNodeId = newLetter + '1';
var renamedNodeDefModel = nodeDefModel.clone(renamedNodeId); // e.g. A3 => B1
analysisDefinitionNodesCollection.invoke('changeSourceIds', nodeId, renamedNodeId);
// We have to manage all widgets associated with the layer due to the operations we do
// creating a new one and managing the previous one
var prevLayerNodeIds = _.pluck(prevLayer.ownedPrimaryAnalysisNodes(), 'id');
var affectedWidgetsBySourceChange = widgetDefinitionsCollection.filter(function (widgetDefModel) {
return _.contains(prevLayerNodeIds, widgetDefModel.get('source'));
});
_.each(affectedWidgetsBySourceChange, function (widgetDefModel) {
var attrs;
var widgetSourceId = widgetDefModel.get('source');
// If this widget doesn't point to the "dragged" node, it will
// keep the source but the layer_id will be totally different
if (widgetSourceId !== nodeId) {
attrs = _.extend(
_.omit(widgetDefModel.toJSON(), 'id', 'layer_id', 'order'),
{
avoidNotification: true
}
);
} else {
// On the other hand, if there is a widget pointing to the dragged node,
// it will keep the layer_id but the source will be different
attrs = _.extend(
_.omit(widgetDefModel.toJSON(), 'id', 'source', 'order'),
{
source: {
id: renamedNodeId
},
avoidNotification: true
}
);
}
affectedWidgetAttrsBySourceChange.push(attrs);
widgetDefModel.attributes.avoidNotification = true;
widgetDefModel.destroy();
});
analysisDefinitionsCollection.newAnalysisForNode(renamedNodeDefModel); // will be saved by saveAnalysisForLayer later since containing that layer's node
// Modify all layers using the old node as top source and it doesn't have an analysis definition associated
layerDefinitionsCollection.each(function (layerDefModel) {
var containsAnalysisDefinition = analysisDefinitionsCollection.findAnalysisForLayer(layerDefModel);
if (!containsAnalysisDefinition && layerDefModel.get('source') === nodeId) {
layerDefModel
.set('source', renamedNodeId)
.save();
}
});
// New layer takes over the identity of the old layer (A), and its primary source as its head node
// We apply the styles from the new header node (A2) if available
oldNodeStyle = nodeDefModel.getPrimarySource().getStyleHistoryForLayer(prevLayer.id);
tableName = prevLayer.get('table_name');
tableNameAlias = prevLayer.get('table_name_alias');
ownerName = prevLayer.get('user_name') || userModel.get('username');
attrs = createDefaultCartoDBAttrs(oldNodeStyle);
attrs.options = _.extend({}, attrs.options, {
sql: 'SELECT * FROM ' + tableName,
table_name: tableName,
table_name_alias: tableNameAlias,
letter: prevLetter,
color: layerColors.getColorForLetter(prevLetter),
source: nodeDefModel.getPrimarySource().id
});
if (ownerName) {
attrs.options.user_name = ownerName;
}
// Tell the backend that this layer is a copy of the old one, and how to rename the analysis nodes.
// This is used to keep the style_history valid in both the old a new layer
attrs.from_layer_id = prevLayer.get('id');
attrs.from_letter = prevLetter;
var newLayerDefModel = layerDefinitionsCollection.add(attrs, { at: prevOrder });
var saveAnalysis = analysisDefinitionsCollection.saveAnalysisForLayer(newLayerDefModel);
$.when(saveAnalysis)
.done(function () {
// Update the layer after the analyses request
prevLayer.save({
letter: newLetter,
color: layerColors.getColorForLetter(newLetter),
source: renamedNodeId
}, { ignoreSchemaChange: true });
})
.fail(function () {
Notifier.addNotification({
status: 'error',
info: _t('notifications.analysis.failed'),
closable: true
});
});
// Remove and add prevLayer (A), to move the layers to expected positions
layerDefinitionsCollection.remove(prevLayer, { silent: true }); // silent to avoid unwanted side-effects; re-added again later
layerDefinitionsCollection.add(prevLayer, { at: newPosition });
// Reset styles from previous layer
this._resetStylePerNode(prevLayer.getAnalysisDefinitionNodeModel(), prevLayer);
newLayerDefModel.save(null, {
success: function () {
restoreWidgetsFromLayer(affectedWidgetAttrsBySourceChange, newLayerDefModel, onNewLayerSaved, !oldNodeStyle);
},
error: function () {
Notifier.addNotification({
status: 'error',
info: _t('notifications.layer.error'),
closable: true
});
}
});
nodeDefModel.destroy(); // replaced by new node
} else {
// Node is the source of a prevLayer
// Create a new layer which simply points to that source
// _______ _______ ______
// | A | | A | | B |
// | | | | | |
// | [A0] | => | [A0] | | [A0] |
// |______| |______| |______|
movementType = '2';
// Keep the same style in the new layer
oldNodeStyle = {
options: {
tile_style: prevLayer.get('cartocss'),
sql_wrap: prevLayer.get('sql_wrap'),
style_properties: prevLayer.get('style_properties')
},
infowindow: prevLayer.get('infowindow'),
tooltip: prevLayer.get('tooltip')
};
tableName = prevLayer.get('table_name');
tableNameAlias = prevLayer.get('table_name_alias');
ownerName = prevLayer.get('user_name') || userModel.get('username');
attrs = createDefaultCartoDBAttrs(oldNodeStyle);
attrs.options = _.extend({}, attrs.options, {
sql: 'SELECT * FROM ' + tableName,
table_name: tableName,
table_name_alias: tableNameAlias,
letter: newLetter,
color: layerColors.getColorForLetter(newLetter),
source: nodeId
});
if (ownerName) {
attrs.options.user_name = ownerName;
}
newLayerDefModel = layerDefinitionsCollection.create(attrs, {
at: newPosition,
success: function () {
onNewLayerSaved(newLayerDefModel, false);
},
error: function () {
Notifier.addNotification({
status: 'error',
info: _t('notifications.layer.error'),
closable: true
});
}
});
}
} else {
// Node is NOT a head of a node, e.g. given nodeId is 'a2' this would create a new layer B which takes over the
// ownership of the given node and its underlying nodes
// _______ _______ ______
// | A | | A | | B |
// | | | | | |
// | [A3] | | [A3] | | {B2} |
// | {A2} | => | {B2} | | [B1] |
// | [A1] | | | | [B0] |
// | [A0] | | | | |
// |______| |______| |______|
movementType = '3';
var linkedNodesList = nodeDefModel.linkedListBySameLetter();
var moveNode = function (oldNode) {
var newId = nodeIds.changeLetter(oldNode.id, newLetter);
var newNode = oldNode.clone(newId);
var affectedWidgetsBySourceChange = widgetDefinitionsCollection.where({ source: oldNode.id });
analysisDefinitionNodesCollection.invoke('changeSourceIds', oldNode.id, newId);
_.each(affectedWidgetsBySourceChange, function (m) {
// Store attrs from affected widget for creating a new
// instance when layer is created
affectedWidgetAttrsBySourceChange.push(
_.extend(
_.omit(m.toJSON(), 'id', 'layer_id', 'source', 'order'),
{
avoidNotification: true,
source: {
id: newId
}
}
)
);
// Destroy widgets pointing to that source until new layer is created
m.attributes.avoidNotification = true;
m.destroy();
});
return newNode;
};
var newLayerHeadNode = moveNode(linkedNodesList[0]);
_.rest(linkedNodesList).forEach(moveNode);
// Create the new layer (B). Copy the layer style from the dragged node.
var ownerLayer = layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);
oldNodeStyle = nodeDefModel.getStyleHistoryForLayer(ownerLayer.id);
tableName = ownerLayer.get('table_name');
tableNameAlias = ownerLayer.get('table_name_alias');
ownerName = ownerLayer.get('user_name') || userModel.get('username');
attrs = createDefaultCartoDBAttrs(oldNodeStyle);
attrs.options = _.extend({}, attrs.options, {
sql: 'SELECT * FROM ' + tableName,
table_name: tableName,
table_name_alias: tableNameAlias,
letter: newLetter,
color: layerColors.getColorForLetter(newLetter),
source: newLayerHeadNode.id
});
if (ownerName) {
attrs.options.user_name = ownerName;
}
// Tell the backend that this layer is a copy of the old one, and how to rename the analysis nodes.
// This is used to keep the style_history valid in both the old a new layer
attrs.from_layer_id = ownerLayer.get('id');
attrs.from_letter = ownerLayer.get('letter');
newLayerDefModel = layerDefinitionsCollection.add(attrs, { at: newPosition });
$.when(analysisDefinitionsCollection.saveAnalysisForLayer(newLayerDefModel))
.done(function () {
newLayerDefModel.save()
.done(function () {
restoreWidgetsFromLayer(affectedWidgetAttrsBySourceChange, newLayerDefModel, onNewLayerSaved, !oldNodeStyle);
})
.fail(function () {
Notifier.addNotification({
status: 'error',
info: _t('notifications.layer.error'),
closable: true
});
});
})
.fail(function () {
Notifier.addNotification({
status: 'error',
info: _t('notifications.analysis.failed'),
closable: true
});
});
nodeDefModel.destroy(); // replaced by new node
}
deleteOrphanedAnalyses();
window.FS && window.FS.log('log', 'Node dragged out case - ' + movementType);
},
moveLayer: function (d) {
var from = d.from;
var to = d.to;
var movingLayer = layerDefinitionsCollection.at(from);
layerDefinitionsCollection.remove(movingLayer, { silent: true });
layerDefinitionsCollection.add(movingLayer, { at: to, parse: false, silent: true });
var saveAnalysisPromises = layerDefinitionsCollection
.chain()
.map(function (layerDefModel) {
var nodeDefModel = layerDefModel.getAnalysisDefinitionNodeModel();
if (nodeDefModel) {
return analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
}
})
.compact()
.value();
$.when.apply($, saveAnalysisPromises).done(function () { // http://api.jquery.com/jQuery.when/
layerDefinitionsCollection.save({
success: function () {
layerDefinitionsCollection.trigger('layerMoved', movingLayer, from, to);
}
});
});
},
deleteLayer: function (id) {
var layerToDelete = layerDefinitionsCollection.get(id);
if (!layerToDelete.canBeDeletedByUser()) return;
var parentLayer;
var toDestroy = [];
// Iterate over each node in the list, to decide how to remove dependent objects or fold nodes (if possible)
// under another layer's linked nodes list.
var linkedNodesList = layerToDelete.ownedPrimaryAnalysisNodes();
var nodeDefModel;
for (var i = 0; i < linkedNodesList.length; i++) {
nodeDefModel = linkedNodesList[i];
parentLayer = layerDefinitionsCollection.findPrimaryParentLayerToAnalysisNode(nodeDefModel, { exclude: toDestroy });
// No parent layer? delete all dependent objects, since can't move current nodeDefModel elsewhere
if (!parentLayer) {
toDestroy.push(nodeDefModel);
layerDefinitionsCollection.each(function (layer) {
if (layer.containsNode(nodeDefModel)) {
if (!_.contains(toDestroy, layer)) {
toDestroy.push(layer);
}
widgetDefinitionsCollection.each(function (widget) {
if (!_.contains(toDestroy, widget) && widget.containsNode(nodeDefModel)) {
toDestroy.unshift(widget);
}
});
}
});
continue; // to process all sequent nodes in the linekd list, and their (possibly) dependent objects
}
var newParentLinkedNodesList = parentLayer
.ownedPrimaryAnalysisNodes()
.concat(nodeDefModel.linkedListBySameLetter());
// Since will reassign ids from the start of list need to calculate which sequence id to start on
var idSequence = newParentLinkedNodesList.length;
var firstSequence = nodeIds.sequence(newParentLinkedNodesList[0].id);
// If first node has a higher sequence start from that one to avoid issues on renaming nodes
if (idSequence <= firstSequence) {
idSequence = firstSequence;
}
var lastNode = _.last(newParentLinkedNodesList);
if (lastNode.get('type') !== 'source') {
idSequence++; // to start on right id sequence, so last item in newParentLinkedNodesList gets assigned x1 as id, since its source will be a none-source node belong to another layer
}
// Reassign ids one-by-one, from start of list to avoid ids overlapping (e.g. [c2,c1]+[b2,b1] = [c4,c3,c2,c1])
var prevId = parentLayer.get('letter') + idSequence;
var moveNodeToParentLayer = function (node) {
var oldNodeId = node.id;
var newId = nodeIds.prev(prevId);
node.set('id', newId);
// Update any depending objects' source
analysisDefinitionNodesCollection.each(function (m) {
if (!_.contains(toDestroy, m)) {
m.changeSourceIds(oldNodeId, newId, true);
}
});
var maybeUpdateSource = function (m) {
if (m.get('source') === oldNodeId && !_.contains(toDestroy, m)) {
m.save({ source: newId }, { silent: true });
}
};
layerDefinitionsCollection.each(maybeUpdateSource);
widgetDefinitionsCollection.each(maybeUpdateSource);
prevId = newId;
return node;
};
var newParentLayerNode = moveNodeToParentLayer(newParentLinkedNodesList[0]);
_.rest(newParentLinkedNodesList).forEach(moveNodeToParentLayer);
parentLayer.save({ source: newParentLayerNode.id });
break; // since the remaining nodes have been move to a parent layer
}
if (!_.contains(toDestroy, layerToDelete)) {
toDestroy.push(layerToDelete);
}
var promise = $.when.apply($, // http://api.jquery.com/jQuery.when/
_.chain(toDestroy)
.reduce(function (memo, m) {
memo.push(m);
var layerDefModel = layerDefinitionsCollection.get(m.id);
if (layerDefModel) { // Also delete analyses associated with layers that are being deleted
var aDefModel = analysisDefinitionsCollection.findAnalysisForLayer(layerDefModel);
if (aDefModel) {
memo.push(aDefModel);
}
}
return memo;
}, [])
.unique()
.map(function (m) {
m.set({ avoidNotification: true }, { silent: true });
return m.destroy();
})
.value());
if (parentLayer) {
analysisDefinitionsCollection.saveAnalysisForLayer(parentLayer);
}
deleteOrphanedAnalyses();
return promise;
},
/**
* E.g. for styles, infowindows etc.
* @param {object} layerDefModel - layer-definition-model
* @param {object} options
*/
saveLayer: function (layerDefModel, options) {
if (!layerDefModel) throw new Error('layerDefModel is required');
if (layerDefModel.isDataLayer()) {
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
}
deleteOrphanedAnalyses();
var saveDefaultOptions = {
shouldPreserveAutoStyle: false
};
options = _.extend({}, saveDefaultOptions, options);
return layerDefModel.save(null, options);
}
};
};

View File

@@ -0,0 +1,13 @@
var Backbone = require('backbone');
var GroupModel = require('./group-model');
/**
* Collection of a User's groups.
*/
module.exports = Backbone.Collection.extend({
model: GroupModel,
initialize: function (models, opts) {
this.organization = opts.organization;
}
});

View File

@@ -0,0 +1,224 @@
var Backbone = require('backbone');
var _ = require('underscore');
var OrganizationModel = require('./organization-model');
var UserGroups = require('./user-groups-collection');
var CustomBaselayersCollection = require('./custom-baselayers-collection');
/**
* User model
*
*/
var UserModel = Backbone.Model.extend({
urlRoot: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v1/users';
},
defaults: {
avatar_url: 'http://cartodb.s3.amazonaws.com/static/public_dashboard_default_avatar.png',
username: '',
email: ''
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
attrs = attrs || {};
if (!_.isEmpty(this.get('organization'))) {
this._organizationModel = new OrganizationModel(
this.get('organization'),
{
configModel: this._configModel,
currentUserId: this.id
}
);
}
if (this.get('layers')) {
this.layers = new CustomBaselayersCollection(this.get('layers'), {
configModel: this._configModel,
currentUserId: this.id
});
}
this.groups = new UserGroups(attrs.groups, {
organization: this._organizationModel
});
},
isViewer: function () {
return this.get('viewer') === true;
},
isBuilder: function () {
return !this.isViewer();
},
canCreateDatasets: function () {
if (this.hasRemainingByteQuota() && this.hasRemainingTableCount()) {
return true;
}
return false;
},
hasRemainingByteQuota: function () {
return !(!this.get('remaining_byte_quota') || this.get('remaining_byte_quota') <= 0);
},
isIndividualUser: function () {
const proUsers = ['Individual', 'Annual Individual'];
return proUsers.indexOf(this.get('account_type')) > -1;
},
hasRemainingTableCount: function () {
if (this.isIndividualUser()) {
return !(!this.get('table_count') || this.get('table_count') >= this.get('table_quota'));
}
return true;
},
updateTableCount: function () {
var currentTableCount = this.get('table_count');
if (currentTableCount) {
this.set('table_count', currentTableCount + 1);
}
},
hasCreateMapsFeature: function () {
return this.isBuilder();
},
hasRemainingPublicMaps: function () {
if (this.isIndividualUser()) {
return this.get('public_map_quota') > this.getTotalPublicMapsCount();
}
return true;
},
getTotalPublicMapsCount: function () {
var totalPublicPrivacyMapsCount = this.get('public_privacy_map_count') || 0;
var totalPasswordPrivacyMapsCount = this.get('password_privacy_map_count') || 0;
var totalLinkPrivacyMapsCount = this.get('link_privacy_map_count') || 0;
return totalPublicPrivacyMapsCount + totalPasswordPrivacyMapsCount + totalLinkPrivacyMapsCount;
},
canCreateTwitterDataset: function () {
var twitter = this.get('twitter');
return !(twitter.quota - twitter.monthly_use) <= 0 && twitter.hard_limit;
},
hasOwnTwitterCredentials: function () {
var twitter = this.get('twitter');
return (twitter && twitter.customized_config) || false;
},
canSelectPremiumOptions: function (visModel) {
return this.get('actions')[ visModel.isVisualization() ? 'private_maps' : 'private_tables' ];
},
canStartTrial: function () {
return !this.isInsideOrg() && this.get('account_type') === 'FREE' && this.get('table_count') > 0;
},
isActionEnabled: function (action) {
return this.get('actions') && this.get('actions')[action];
},
canCreatePrivateDatasets: function () {
var actions = this.get('actions');
return actions && actions.private_tables;
},
isInsideOrg: function () {
if (this._organizationModel) {
return !!this._organizationModel.id || this.isOrgOwner();
}
return false;
},
isOrgOwner: function () {
if (this._organizationModel) {
return this._organizationModel.getOwnerId() === this.get('id');
}
return false;
},
isOrgAdmin: function () {
if (this._organizationModel) {
return this._organizationModel.isOrgAdmin(this);
}
return false;
},
hasAccountType: function (accountType) {
return this.get('account_type') === accountType;
},
featureEnabled: function (name) {
var featureFlags = this.get('feature_flags');
if (!featureFlags || featureFlags.length === 0 || !name) {
return false;
}
return _.contains(featureFlags, name);
},
upgradeContactEmail: function () {
if (this.isInsideOrg()) {
if (this.isOrgOwner()) {
return 'enterprise-support@carto.com';
} else {
return this._organizationModel.getOwnerEmail();
}
} else {
return 'support@carto.com';
}
},
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 '';
},
getMaxConcurrentImports: function () {
return (this.get('limits') && this.get('limits').concurrent_imports) || 1;
},
getSchemaName: function () {
return this.isInsideOrg() ? this.get('username') : 'public';
},
renderData: function (currentUser) {
var name = this.get('username');
if (currentUser && currentUser.id === this.id) {
name = _t('user.you');
}
return {
username: name,
avatar_url: this.get('avatar_url')
};
},
clone: function () {
var attrs = _.clone(this.attributes);
delete attrs.id;
return new UserModel(attrs, {
configModel: this._configModel
});
},
getOrganization: function () {
return this._organizationModel;
}
});
module.exports = UserModel;

View File

@@ -0,0 +1,39 @@
var _ = require('underscore');
var Backbone = require('backbone');
/**
* User Notifications
*
*/
var UserNotifications = Backbone.Model.extend({
sync: function (method, model, options) {
return Backbone.sync('update', model, options);
},
url: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v3/notifications/' + this.get('key');
},
initialize: function (attrs, opts) {
if (!opts.key) throw new Error('key is required');
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
this.attributes = _.extend({ notifications: attrs }, { key: opts.key });
},
getKey: function (key) {
var notifications = this.get('notifications') || {};
return notifications[key];
},
setKey: function (key, value) {
var notifications = this.get('notifications') || {};
notifications[key] = value;
this.set('notifications', notifications);
}
});
module.exports = UserNotifications;

View File

@@ -0,0 +1,35 @@
var Backbone = require('backbone');
var syncAbort = require('./backbone/sync-abort');
/**
* A collection representing a set of users in a group.
*/
module.exports = Backbone.Collection.extend({
sync: syncAbort,
initialize: function (models, opts) {
if (!opts.group) throw new Error('group is required');
this.group = opts.group;
this.configModel = opts.configModel;
this.organization = opts.organization;
},
url: function (method) {
var baseUrl = this.configModel.get('base_url');
var version = this.configModel.urlVersion('organizationGroups', method);
return baseUrl + '/api/' + version + '/organization/' + this.organization.id + '/groups/' + this.group.id + '/users';
},
parse: function (response) {
this.total_entries = response.total_entries;
this.total_user_entries = response.total_user_entries;
return response.users;
},
totalCount: function () {
return this.total_user_entries;
}
});

View File

@@ -0,0 +1,50 @@
var $ = require('jquery');
var _ = require('underscore');
var UsersGroup = require('./users-group-collection');
// This module fetch the user for every group present
// in the ACL permission collection. The vizjson doesn't
// provide and we need it for the sharing stats in the header.
module.exports = {
track: function (opts) {
this.acl = opts.acl;
this.configModel = opts.configModel;
this.userModel = opts.userModel;
this.acl.on('reset', this.fetchUsers, this);
this.fetchUsers();
},
fetchUsers: function () {
var self = this;
var promises = [];
promises = _.map(this.acl.where({type: 'group'}), function (group) {
var entity = group.get('entity');
var deferred = new $.Deferred();
if (!entity.users || entity.users.length === 0) {
entity.users = new UsersGroup([], {
group: entity,
configModel: self.configModel,
organization: self.userModel.getOrganization()
});
entity.users.fetch({
success: function () {
deferred.resolve();
}
});
} else {
deferred.resolve();
}
return deferred.promise();
}, this);
$.when.apply($, promises).done(function () {
self.acl.trigger('fetch');
});
}
};

View File

@@ -0,0 +1,95 @@
var _ = require('underscore');
var Backbone = require('backbone');
var PermissionModel = require('./permission-model');
/**
* Model that represents a visualization (v3)
*
* Even though a table might be represented as a Visualization in some cases, please use TableModel if you want to work
* with the table data instead of adding table-specific methods here.
*/
var PRIVACY_OPTIONS = {
public: 'PUBLIC',
link: 'LINK',
private: 'PRIVATE',
password: 'PASSWORD'
};
module.exports = Backbone.Model.extend({
defaults: {
visChanges: 0
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
if (this.get('permission')) {
this._permissionModel = new PermissionModel(this.get('permission'), {
configModel: opts.configModel
});
this.unset('permission');
}
},
urlRoot: function () {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('visualization');
return baseUrl + '/api/' + version + '/viz';
},
mapcapsURL: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v3/viz/' + this.id + '/mapcaps';
},
embedURL: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/builder/' + this.id + '/embed';
},
builderURL: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/builder/' + this.id + '/';
},
vizjsonURL: function () {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('vizjson', 'read', 'v3');
return baseUrl + '/api/' + version + '/viz/' + this.id + '/viz.json';
},
stateURL: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v3/viz/' + this.id + '/state';
},
isVisualization: function () {
return this.get('type') === 'derived';
},
privacyOptions: function () {
return _.values(PRIVACY_OPTIONS);
},
recordChange: function () {
var visChanges = this.get('visChanges');
this.set('visChanges', visChanges + 1);
},
resetChanges: function () {
this.set('visChanges', 0);
},
getPermissionModel: function () {
return this._permissionModel;
}
}, {
isPubliclyAvailable: function (privacyStatus) {
return privacyStatus === PRIVACY_OPTIONS.password ||
privacyStatus === PRIVACY_OPTIONS.link ||
privacyStatus === PRIVACY_OPTIONS.public;
}
});

View File

@@ -0,0 +1,22 @@
var Backbone = require('backbone');
/**
* Edit vis metadata dialog model
* to control if name and metadata
* are editable or not.
*
*/
module.exports = Backbone.Model.extend({
defaults: {
name: ''
},
validate: function (attrs) {
if (!attrs) return;
if (!attrs.name) {
return _t('components.modals.maps-metadata.validation.error.name');
}
}
});

View File

@@ -0,0 +1,93 @@
var Backbone = require('backbone');
var _ = require('underscore');
var TableModel = require('./table-model');
var PermissionModel = require('./permission-model');
var SynchronizationModel = require('./synchronization-model');
/**
* Model that represents a table visualization (v3)
*
*/
var PRIVACY_OPTIONS = ['PUBLIC', 'LINK', 'PRIVATE'];
module.exports = Backbone.Model.extend({
urlRoot: function () {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('visualization');
return baseUrl + '/api/' + version + '/viz';
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
this._initModels();
},
_initModels: function () {
var d = this.get('table');
if (!_.isEmpty(this.get('external_source'))) {
d = this.get('external_source');
}
this._tableModel = new TableModel(d, {
configModel: this._configModel,
parse: true
});
if (this.get('permission')) {
this._permissionModel = new PermissionModel(this.get('permission'), {
configModel: this._configModel
});
this.unset('permission');
}
if (this.get('synchronization')) {
this._synchronizationModel = new SynchronizationModel(this.get('synchronization'), {
configModel: this._configModel
});
this._synchronizationModel.linkToTable(this._tableModel);
}
},
isVisualization: function () {
return false;
},
getTableModel: function () {
return this._tableModel;
},
getPermissionModel: function () {
return this._permissionModel;
},
getSynchronizationModel: function () {
return this._synchronizationModel;
},
vizjsonURL: function () {
var baseUrl = this._configModel.get('base_url');
var version = this._configModel.urlVersion('vizjson', 'read', 'v3');
return baseUrl + '/api/' + version + '/viz/' + this.id + '/viz.json';
},
isRaster: function () {
return this.get('kind') === 'raster';
},
datasetURL: function () {
var baseUrl = this._configModel.get('base_url');
var tableName = this.getTableModel().getUnquotedName();
return baseUrl + '/dataset/' + tableName;
},
privacyOptions: function () {
return PRIVACY_OPTIONS;
},
toJSON: function () {
return _.omit(this.attributes, 'synchronization', 'stats', 'table');
}
});

View File

@@ -0,0 +1,78 @@
var _ = require('underscore');
var Backbone = require('backbone');
var VisualizationTableModel = require('./visualization-table-model');
var VisualizationModel = require('./vis-definition-model');
/**
* A collection that holds visualization models
*/
module.exports = Backbone.Collection.extend({
DEFAULT_FETCH_OPTIONS: {
type: 'table',
order: 'updated_at',
page: 1,
per_page: 20,
exclude_shared: false,
exclude_raster: true,
tags: '',
q: ''
},
model: function (d, opts) {
var configModel = opts.collection._configModel;
var Klass;
if (d.type !== 'derived') {
Klass = VisualizationTableModel;
} else {
Klass = VisualizationModel;
}
return new Klass(d, {
configModel: configModel
});
},
initialize: function (models, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
// this.DEFAULT_FETCH_OPTIONS = _.extendOwn(this.DEFAULT_FETCH_OPTIONS, opts);
this._stats = {};
},
url: function () {
var version = this._configModel.urlVersion('visualization');
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/' + version + '/viz';
},
// Overrides the default fetch, to use the internal methods to construct parmas
fetch: function (opts) {
this.trigger('loading', this);
opts = opts || {
data: {
// If reaches this code path it's because there were no opts given, i.e. should do a 'full fetch'
// Since there is no current way to really do a full fetch let's just set a really high number to get all…
// TODO this is obviously bad for organization users, how can we do this differently
per_page: 1000
}
};
opts.data = _.extend({}, this.DEFAULT_FETCH_OPTIONS, opts.data);
return Backbone.Collection.prototype.fetch.call(this, opts);
},
getDefaultParam: function (param) {
return this.DEFAULT_FETCH_OPTIONS[param];
},
getTotalStat: function (attribute) {
return this._stats[attribute] || 0;
},
parse: function (res) {
this._stats = _.omit(res, 'visualizations');
return res.visualizations;
}
});

View File

@@ -0,0 +1,36 @@
var Backbone = require('backbone');
/**
* Model that encapsulates params for fetching data in a cdb.admin.Visualizations collection.
*/
module.exports = Backbone.Model.extend({
defaults: {
content_type: '',
page: 1,
q: '',
tag: '',
category: '',
shared: 'no',
locked: false,
liked: false,
library: false,
order: 'updated_at',
deepInsights: false
},
isSearching: function () {
return this.get('q') || this.get('tag');
},
isDatasets: function () {
return this.get('content_type') === 'datasets';
},
isMaps: function () {
return this.get('content_type') === 'maps';
},
isDeepInsights: function () {
return this.isMaps() && this.get('deepInsights');
}
});

View File

@@ -0,0 +1,208 @@
var _ = require('underscore');
var Backbone = require('backbone');
var CartoColor = require('cartocolor');
var syncAbort = require('./backbone/sync-abort');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'configModel',
'mapId'
];
var ATTRS_AT_TOP_LEVEL = ['id', 'layer_id', 'source', 'title', 'type', 'order'];
var STYLE_PROPERTIES = ['widget_style_definition', 'auto_style_definition', 'auto_style_allowed', 'widget_color_changed'];
var DEFAULT_WIDGET_COLOR = '#9DE0AD';
var TIME_SERIES_WIDGET_COLOR = '#F2CC8F';
var DEFAULT_WIDGET_STYLE = {
color: {
fixed: DEFAULT_WIDGET_COLOR,
opacity: 1
}
};
var DEFAULT_RAMP_SIZE = 256;
var getDefaultCategoriesByRange = function (range) {
var widgetDomain = [];
for (var i = 0, l = range.length; i < l; i++) {
widgetDomain.push(_t('form-components.editors.fill.quantification.methods.category') + ' #' + (i + 1));
}
return widgetDomain;
};
/**
* Widget definition Model
*/
module.exports = Backbone.Model.extend({
defaults: {
sync_on_bbox_change: true,
auto_style_allowed: true,
widget_style_definition: DEFAULT_WIDGET_STYLE,
widget_color_changed: false
},
/**
* @override {Backbone.prototype.sync} abort ongoing request if there is any
*/
sync: syncAbort,
initialize: function (attrs, opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
},
urlRoot: function () {
// Since widget are stored under layers the collection can't figure out the URL by itself,
// thus each widget sets its own urlRoot
var baseUrl = this._configModel.get('base_url');
var layerId = this.get('layer_id');
return baseUrl + '/api/v3/maps/' + this._mapId + '/layers/' + layerId + '/widgets';
},
/**
* The API response format is a little bit different, and may
* @param {Object} API response
* @return {Object} attrs to be set on model
*/
parse: function (response) {
var attrs = _.defaults(
_.pick(response, ATTRS_AT_TOP_LEVEL),
response.options
);
if (!_.isEmpty(response.style)) {
var style = JSON.parse(JSON.stringify(response.style));
if (style.auto_style) {
var autoStyle = style.auto_style;
var autoStyleDefinition = autoStyle.definition;
if (autoStyleDefinition && autoStyleDefinition.color && autoStyleDefinition.color.quantification === 'category') {
autoStyle.definition.color.domain = getDefaultCategoriesByRange(autoStyleDefinition.color.range);
}
attrs.auto_style_definition = autoStyle.definition;
attrs.auto_style_allowed = autoStyle.allowed;
}
attrs.widget_style_definition = response.style.widget_style.definition;
attrs.widget_color_changed = response.style.widget_style.widget_color_changed;
}
attrs.source = attrs.source && attrs.source.id;
// Fetch column type
var columnTypeInSource;
if (attrs.column && attrs.source) {
columnTypeInSource = this.collection.getColumnType(attrs.column, attrs.source);
}
if (columnTypeInSource && columnTypeInSource !== attrs.column_type) {
attrs.column_type = columnTypeInSource;
}
return attrs;
},
/**
* @override Backbone.Model.prototype.toJSON
* Formats the JSON to match the server-side API
*/
toJSON: function () {
var attributes = _.pick(this.attributes, ATTRS_AT_TOP_LEVEL);
attributes.source = {id: this.get('source')};
attributes.options = _.omit(this.attributes, ATTRS_AT_TOP_LEVEL, STYLE_PROPERTIES);
if (this.get('widget_style_definition')) {
attributes.style = {
widget_style: {
definition: this.get('widget_style_definition'),
widget_color_changed: this.get('widget_color_changed')
}
};
attributes.style.auto_style = {
custom: !!this.get('auto_style_definition'),
allowed: this.get('auto_style_allowed')
};
if (this.get('auto_style_definition')) {
var autoStyleDefinition = JSON.parse(JSON.stringify(this.get('auto_style_definition')));
autoStyleDefinition.color = _.omit(autoStyleDefinition.color, 'domain', 'bins');
attributes.style.auto_style.definition = autoStyleDefinition;
}
}
return attributes;
},
changeType: function (type) {
var resetableAttrs = this.collection.resetableAttrsForTypeMap(type);
_.each(resetableAttrs, function (attr) {
this.unset(attr, { silent: true });
}, this);
var attrsForNewType = _.defaults({ type: type }, this.collection.attrsForThisType(type, this));
// Unset now irrelevant attributes due to the type change
_
.chain(this.attributes)
.keys()
.difference(
['sync_on_bbox_change'].concat(ATTRS_AT_TOP_LEVEL, STYLE_PROPERTIES),
_.keys(attrsForNewType)
)
.each(function (key) {
this.unset(key, { silent: true });
}, this);
this.set(attrsForNewType);
},
containsNode: function (otherNodeDefModel) {
if (!otherNodeDefModel) return false;
var sourceId = this.get('source');
var nodeDefModel = otherNodeDefModel.collection.get(sourceId);
return !!(sourceId === otherNodeDefModel.id ||
nodeDefModel && nodeDefModel.containsNode(otherNodeDefModel));
}
}, {
getDefaultWidgetStyle: function (type) {
var widgetColor = type === 'time-series' ? TIME_SERIES_WIDGET_COLOR : DEFAULT_WIDGET_COLOR;
var defaultWidgetStyle = DEFAULT_WIDGET_STYLE;
defaultWidgetStyle.color.fixed = widgetColor;
return defaultWidgetStyle;
},
getDefaultAutoStyle: function (type, columnName) {
var defaultWidgetCategories = 10;
var defaultWidgetRampSize = 7;
var widgetRamp = _.first(
_.compact(
_.map(CartoColor, function (ramp) {
return _.clone(ramp[defaultWidgetRampSize]);
}, this)
)
);
var widgetCategories = _.clone(CartoColor.Prism[defaultWidgetCategories]);
var widgetRange = type === 'category' ? widgetCategories : widgetRamp;
var widgetQuantification = type === 'category' ? 'category' : 'quantiles';
var attrs = {
color: {
attribute: columnName,
quantification: widgetQuantification,
range: widgetRange
}
};
if (type === 'category') {
attrs.color.domain = getDefaultCategoriesByRange(widgetCategories);
} else {
attrs.color.bins = type === 'time-series'
? DEFAULT_RAMP_SIZE
: defaultWidgetRampSize;
}
return attrs;
}
});

View File

@@ -0,0 +1,220 @@
var _ = require('underscore');
var Backbone = require('backbone');
var WidgetDefinitionModel = require('./widget-definition-model');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'configModel',
'mapId',
'layerDefinitionsCollection',
'analysisDefinitionNodesCollection'
];
/**
* Collection of widget definitions, synhronizes the internal definitions and the widget models.
*/
module.exports = Backbone.Collection.extend({
comparator: 'order',
model: function (attrs, opts) {
var self = opts.collection;
attrs.order = !_.isUndefined(attrs.order) ? attrs.order : 0;
var model = new WidgetDefinitionModel(attrs, {
parse: true, // make sure data is structured as expected
collection: self,
configModel: self._configModel,
mapId: self._mapId
});
return model;
},
initialize: function (models, opts) {
var self = this;
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._bindLayerStyleChanges();
this._attrsForThisTypeMap = {
formula: function (model) {
return {
column: model.get('column'),
operation: model.get('operation') || 'max'
};
},
category: function (model) {
var columnName = model.get('column');
return {
aggregation: model.get('aggregation') || 'count',
aggregation_column: model.get('aggregation_column') || columnName,
column: columnName,
widget_style_definition: model.get('widget_style_definition') || WidgetDefinitionModel.getDefaultWidgetStyle('category'),
auto_style_definition: undefined
};
},
histogram: function (model) {
return {
column: model.get('column'),
bins: model.get('bins') || 10,
column_type: 'number',
widget_style_definition: model.get('widget_style_definition') || WidgetDefinitionModel.getDefaultWidgetStyle('histogram'),
auto_style_definition: undefined
};
},
'time-series': function (model) {
return {
column: model.get('column'),
bins: model.get('bins') || 256,
aggregation: model.get('aggregation'),
column_type: self.getColumnType(model.get('column'), model.get('source')),
widget_style_definition: model.get('widget_style_definition') || WidgetDefinitionModel.getDefaultWidgetStyle('time-series')
};
}
};
},
url: function () {
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/v3/maps/' + this._mapId + '/widgets';
},
parse: function (response) {
return response && response.widgets
? response.widgets
: [];
},
_bindLayerStyleChanges: function () {
this._layerDefinitionsCollection.bind('change:style_properties', this._onLayerStyleChanged, this);
},
_onLayerStyleChanged: function (layerDefModel) {
var styleModel = layerDefModel.styleModel;
var isAllowed = styleModel && styleModel.canApplyAutoStyle();
var affectedWidgets = this.where({ layer_id: layerDefModel.id });
_.each(affectedWidgets, function (widgetDefModel) {
var isNowAllowed = widgetDefModel.get('auto_style_allowed');
var willBeAllowed = isAllowed;
var attrs = {
auto_style_allowed: willBeAllowed
};
if (isNowAllowed !== willBeAllowed) {
// if autostyle is not allow, we should clean auto_style_definition
if (willBeAllowed === false) {
attrs = _.extend(attrs, { auto_style_definition: '' });
}
widgetDefModel.save(attrs);
}
});
},
addWidget: function (widgetModel, attrs) {
var self = this;
var ORDER_NUMBER_TO_APPEAR_ON_TOP = -1;
return new Promise(function (resolve, reject) {
_.extend(attrs, { order: ORDER_NUMBER_TO_APPEAR_ON_TOP });
self.create(attrs, {
wait: true,
success: function (model) {
resolve(model);
},
error: function (response, error) {
reject(error);
}
});
});
},
updateWidgetsOrder: function (numberOfWidgets) {
this.each(function (widgetDefinitionModel, index) {
widgetDefinitionModel.set({ order: index });
});
return this.saveAsync(numberOfWidgets);
},
saveAsync: function (updatedWidgets) {
var self = this;
this.trigger('loading', updatedWidgets);
return new Promise(function (resolve, reject) {
self.save({
wait: true,
success: function (collection) {
self.trigger('successAdd', updatedWidgets);
resolve(collection);
},
error: function (response, error) {
self.trigger('errorAdd', response);
reject(error);
}
});
});
},
save: function (options) {
options = options || {};
Backbone.sync('update', this, options);
},
attrsForThisType: function (newType, m) {
return this._attrsForThisTypeMap[newType](m);
},
resetableAttrsForTypeMap: function (type) {
switch (type) {
case 'formula':
return [];
case 'category':
return ['aggregation'];
case 'histogram':
return ['bins', 'column_type'];
case 'time-series':
return ['bins', 'aggregation', 'column_type'];
}
},
isThereTimeSeries: function (opts) {
opts = opts || {};
var timeSeries = opts.animated ? this.findWhere({ type: 'time-series', animated: opts.animated }) : this.findWhere({ type: 'time-series' });
return !!timeSeries;
},
isThereOtherWidgets: function () {
return !!this.find(function (widgetModel) {
return widgetModel.get('type') !== 'time-series';
});
},
_getNextOrder: function () {
if (this.isEmpty()) {
return 0;
} else {
var lastItemByOrder = this.max(function (mdl) {
return mdl.get('order');
});
return lastItemByOrder.get('order') + 1;
}
},
widgetsOwnedByLayer: function (layerId) {
var widgets = this.where({ layer_id: layerId });
return widgets.length;
},
getColumnType: function (columnName, source) {
var column;
var node = this._analysisDefinitionNodesCollection.get(source);
var schemaModel = node && node.querySchemaModel;
if (schemaModel && schemaModel.get('status') === 'fetched') {
column = schemaModel.columnsCollection.findWhere({ name: columnName });
}
return column && column.get('type');
}
});

Some files were not shown because too many files have changed in this diff Show More