/* * this model is created to manage the visualization order. In order to simplify API * the order is changed using a double linked list instead of a order attribute. */ cdb.admin.VisualizationOrder = cdb.core.Model.extend({ url: function(method) { return this.visualization.url(method) + "/next_id" }, initialize: function () { this.visualization = this.get('visualization'); //set id so PUT is used this.set('id', this.visualization.id); this.unset('visualization'); } }); cdb.admin.Visualization = cdb.core.Model.extend({ defaults: { bindMap: true }, url: function(method) { var version = cdb.config.urlVersion('visualization', method); var base = '/api/' + version + '/viz'; if (this.isNew()) { return base; } return base + '/' + this.id; }, INHERIT_TABLE_ATTRIBUTES: [ 'name', 'description', 'privacy' ], initialize: function() { this.map = new cdb.admin.Map(); this.permission = new cdb.admin.Permission(this.get('permission')); this.overlays = new cdb.admin.Overlays([]); this.overlays.vis = this; this.initSlides(); this.bind('change:type', this.initSlides); this.transition = new cdb.admin.SlideTransition(this.get('transition_options'), { parse: true }); this.order = new cdb.admin.VisualizationOrder({ visualization: this }); // Check if there are related tables and generate the collection if (this.get('type') === "derived" && this.get('related_tables')) this.generateRelatedTables(); // Check if there are dependant visualizations and generate the collection // // TODO // if (this.get('bindMap')) this._bindMap(); this.on(_(this.INHERIT_TABLE_ATTRIBUTES).map(function(t) { return 'change:' + t }).join(' '), this._changeAttributes, this); this._initBinds(); }, initSlides: function() { if (this.slides) return this; // slides only for derived visualizations // and working with map enabled if (this.get('type') === 'derived' && this.get('bindMap')) { this.slides = new cdb.admin.Slides(this.get('children'), { visualization: this }); this.slides.initializeModels(); } else { this.slides = new cdb.admin.Slides([], { visualization: this }); } return this; }, activeSlide: function(c) { if (c >= 0 && c < this.slides.length) { this.slides.setActive(this.slides.at(c)); } return this; }, // set master visualization. Master manages id, name and description changes setMaster: function(master_vis) { var self = this; master_vis.bind('change:id', function() { self.changeTo(master_vis); self.slides.master_visualization_id = master_vis.id; }, this); master_vis.bind('change', function() { var c = master_vis.changedAttributes(); if (c.type) self.set('type', master_vis.get('type')); self.set('description', master_vis.get('description')); if (c.name) self.set('name', master_vis.get('name')); if (c.privacy) self.set('privacy', master_vis.get('privacy')); }); }, enableOverlays: function() { this.bind('change:id', this._fetchOverlays, this); if (!this.isNew()) this._fetchOverlays(); }, _fetchOverlays: function() { this.overlays.fetch({ reset: true }); }, _initBinds: function() { this.permission.acl.bind('reset', function() { // Sync the local permission object w/ the raw data, so vis.save don't accidentally overwrites permissions changes this.set('permission', this.permission.attributes, { silent: true }); this.trigger('change:permission', this); }, this); // Keep permission model in sync, e.g. on vis.save this.bind('change:permission', function() { this.permission.set(this.get('permission')); }, this); }, isLoaded: function() { return this.has('privacy') && this.has('type'); }, generateRelatedTables: function(callback) { var tables = this.get('related_tables'); if (tables.length) { var collection = new Backbone.Collection([]); for (var i = 0, l = tables.length; i < l; i++) { var table = new cdb.admin.CartoDBTableMetadata(tables[i]); collection.add(table); } this.related_tables = collection; callback && callback.success && callback.success(); } }, getRelatedTables: function(callback, options) { options = options || {}; if (this.get('type') === "derived") { if (!options.force && this.get('related_tables')) { this.generateRelatedTables(callback); return; } var self = this; this.fetch({ success: function() { self.generateRelatedTables(callback); }, error: callback && callback.error && callback.error }); } }, /** * Get table metadata related to this vis. * Note that you might need to do a {metadata.fetch()} to get full data. * * @returns {cdb.admin.CartoDBTableMetadata} if this vis represents a table * TODO: when and when isn't it required to do a fetch really? */ tableMetadata: function() { if (!this._metadata) { this._metadata = new cdb.admin.CartoDBTableMetadata(this.get('table')); } return this._metadata; }, _bindMap: function() { this.on('change:map_id', this._fetchMap, this); this.map.bind('change:id', function() { this.set('map_id', this.map.id); }, this); this.map.set('id', this.get('map_id')); // when the layers change we should reload related_tables this.map.layers.bind('change:id remove', function() { this.getRelatedTables(null, { force: true }); }, this); }, /** * Is this model a true visualization? */ isVisualization: function() { return this.get('type') === "derived" || this.get('type') === 'slide'; }, /** * Change current visualization by new one without * creating a new instance. * * When turn table visualization to derived visualization, * it needs to wait until reset layers. If not, adding a new * layer after create the new visualization won't work... * */ changeTo: function(new_vis, callbacks) { this.set(new_vis.attributes, { silent: true }); this.transition.set(new_vis.transition.attributes); var success = function() { this.map.layers.unbind('reset', success); this.map.layers.unbind('error', error); callbacks && callbacks.success && callbacks.success(this); }; var error = function() { this.map.layers.unbind('reset', success); this.map.layers.unbind('error', error); callbacks && callbacks.error && callbacks.error(); } this.map.layers.bind('reset', success, this); this.map.layers.bind('error', error, this) this.permission.set(new_vis.permission.attributes); this.set({ map_id: new_vis.get('map_id') }); // Get related tables from the new visualization this.getRelatedTables(); }, /** * Transform a table visualization/model to a original visualization */ changeToVisualization: function(callback) { var self = this; if (!this.isVisualization()) { var callbacks = { success: function(new_vis) { self.changeTo(new_vis, callback); self.notice('', '', 1000); }, error: function(e) { var msg = 'error changing to visualization'; self.error(msg, e); callback && callback.error(e, msg); } }; // Name is not saved in the back end, due to that // we need to pass it as parameter this.copy({ name: this.get('name'), description: this.get('description') }, callbacks); } else { self.notice('', '', 1000); } return this; }, parse: function(data) { if (this.transition && data.transition_options) { this.transition.set(this.transition.parse(data.transition_options)); } return data; }, toJSON: function() { var attr = _.clone(this.attributes); delete attr.bindMap; delete attr.stats; delete attr.related_tables; delete attr.children; attr.map_id = this.map.id; attr.transition_options = this.transition.toJSON(); return attr; }, /** * Create a child (slide) from current visualization. It clones layers but no overlays */ createChild: function(attrs, options) { attrs = attrs || {}; options = options || {}; var vis = new cdb.admin.Visualization( _.extend({ copy_overlays: false, type: 'slide', parent_id: this.id }, attrs ) ); vis.save(null, options); return vis; }, /** * Create a copy of the visualization model */ copy: function(attrs, options) { attrs = attrs || {}; options = options || {}; var vis = new cdb.admin.Visualization( _.extend({ source_visualization_id: this.id }, attrs ) ); vis.save(null, options); return vis; }, /** * Fetch map information */ _fetchMap: function() { this.map .set('id', this.get('map_id')) .fetch(); }, /** * Generic function to catch up new attribute changes */ _changeAttributes: function(m, c) { if (!this.isVisualization()) { // Change table attribute if layer is CartoDB-layer var self = this; this.map.layers.each(function(layer) { if (layer.get('type').toLowerCase() == "cartodb") { // If there isn't any changed attribute if (!self.changedAttributes()) { return false; } var attrs = _.pick(self.changedAttributes(), self.INHERIT_TABLE_ATTRIBUTES); if (attrs) layer.fetch(); } }) } }, // PUBLIC FUNCTIONS publicURL: function() { var url = this.permission.owner.viewUrl(); return url + "/viz/" + this.get('id') + "/public_map"; }, deepInsightsUrl: function(user) { var url = user.viewUrl(); return url + "/bivisualizations/" + this.get('id') + "/embed_map"; }, embedURL: function() { var url = this.permission.owner.viewUrl(); return url + "/viz/" + this.get('id') + "/embed_map"; }, vizjsonURL: function() { var url = this.permission.owner.viewUrl(); var version = cdb.config.urlVersion('vizjson', 'read', 'v2'); return url + '/api/' + version + '/viz/' + this.get('id') + "/viz.json"; }, notice: function(msg, type, timeout) { this.trigger('notice', msg, type, timeout); }, error: function(msg, resp) { this.trigger('notice', msg, 'error'); }, // return: Array of entities (user or organizations) this vis is shared with sharedWithEntities: function() { return _.map((this.permission.acl.toArray() || []), function(aclItem) { return aclItem.get('entity') }); }, privacyOptions: function() { if (this.isVisualization()) { return cdb.admin.Visualization.ALL_PRIVACY_OPTIONS; } else { return _.reject(cdb.admin.Visualization.ALL_PRIVACY_OPTIONS, function(option) { return option === 'PASSWORD'; }); } }, isOwnedByUser: function(user) { return user.equals(this.permission.owner); }, /** * Get the URL for current instance. * @param {Object} currentUser (Optional) Get the URL from the perspective of the current user, necessary to * correctly setup URLs to tables. * @return {Object} instance of cdb.common.Url */ viewUrl: function(currentUser) { var owner = this.permission.owner; var userUrl = this.permission.owner.viewUrl(); // the undefined check is required for backward compability, in some cases (e.g. dependant visualizations) the type // is not available on the attrs, if so assume the old behavior (e.g. it's a visualization/derived/map). if (this.isVisualization() || _.isUndefined(this.get('type'))) { var id = this.get('id') if (currentUser && currentUser.id !== owner.id && this.permission.hasAccess(currentUser)) { userUrl = currentUser.viewUrl(); id = owner.get('username') + '.' + id; } return new cdb.common.MapUrl({ base_url: userUrl.urlToPath('viz', id) }); } else { if (currentUser && this.permission.hasAccess(currentUser)) { userUrl = currentUser.viewUrl(); } return new cdb.common.DatasetUrl({ base_url: userUrl.urlToPath('tables', this.tableMetadata().getUnquotedName()) }); } }, /** * Returns the URL, server-side generated */ _canonicalViewUrl: function() { var isMap = this.isVisualization() || _.isUndefined(this.get('type')); var UrlModel = isMap ? cdb.common.MapUrl : cdb.common.DatasetUrl; return new UrlModel({ base_url: this.get('url') }); } }, { ALL_PRIVACY_OPTIONS: [ 'PUBLIC', 'LINK', 'PRIVATE', 'PASSWORD' ] }); /** * Visualizations endpoint available for a given user. * * Usage: * * var visualizations = new cdb.admin.Visualizations() * visualizations.fetch(); * */ cdb.admin.Visualizations = Backbone.Collection.extend({ model: cdb.admin.Visualization, _PREVIEW_TABLES_PER_PAGE: 10, _TABLES_PER_PAGE: 20, _PREVIEW_ITEMS_PER_PAGE: 3, _ITEMS_PER_PAGE: 9, initialize: function() { var default_options = new cdb.core.Model({ tag_name : "", q : "", page : 1, type : "derived", exclude_shared : false, per_page : this._ITEMS_PER_PAGE }); // Overrriding default sync, preventing // run several request at the same time this.sync = Backbone.syncAbort; this.options = _.extend(default_options, this.options); this.total_entries = 0; this.options.bind("change", this._changeOptions, this); this.bind("reset", this._checkPage, this); this.bind("update", this._checkPage, this); this.bind("add", this._fetchAgain, this); }, getTotalPages: function() { return Math.ceil(this.total_entries / this.options.get("per_page")); }, _checkPage: function() { var total = this.getTotalPages(); var page = this.options.get('page') - 1; if (this.options.get("page") > total ) { this.options.set({ page: total + 1}); } else if (this.options.get("page") < 1) { this.options.set({ page: 1}); } }, _createUrlOptions: function() { return _.compact(_(this.options.attributes).map( function(v, k) { return k + "=" + encodeURIComponent(v) } )).join('&'); }, url: function(method) { var u = ''; // TODO: remove this workaround when bi-visualizations are included as // standard visualizations if (this.options.get('deepInsights')) { u += '/api/v1/bivisualizations'; u += '?page=' + this.options.get('page') + '&per_page=' + this.options.get("per_page"); } else { var version = cdb.config.urlVersion('visualizations', method); u += '/api/' + version + '/viz/'; u += "?" + this._createUrlOptions(); } return u; }, remove: function(options) { this.total_entries--; this.elder('remove', options); }, // add bindMap: false for all the visulizations // vis model does not need map information in dashboard parse: function(response) { this.total_entries = response.total_entries; this.slides && this.slides.reset(response.children); this.total_shared = response.total_shared; this.total_user_entries = response.total_user_entries; return _.map(response.visualizations, function(v) { v.bindMap = false; return v; }); }, _changeOptions: function() { // this.trigger('updating'); // var self = this; // $.when(this.fetch()).done(function(){ // self.trigger('forceReload') // }); }, create: function(m) { var dfd = $.Deferred(); Backbone.Collection.prototype.create.call(this, m, { wait: true, success: function() { dfd.resolve(); }, error: function() { dfd.reject(); } } ); return dfd.promise(); }, fetch: function(opts) { var dfd = $.Deferred(); var self = this; this.trigger("loading", this); $.when(Backbone.Collection.prototype.fetch.call(this,opts)) .done(function(res) { self.trigger('loaded'); dfd.resolve(); }).fail(function(res) { self.trigger('loadFailed'); dfd.reject(res); }); return dfd.promise(); } });