879 lines
28 KiB
Ruby
879 lines
28 KiB
Ruby
|
require 'forwardable'
|
||
|
require 'virtus'
|
||
|
require 'json'
|
||
|
require 'cartodb-common'
|
||
|
require_relative '../markdown_render'
|
||
|
require_relative './presenter'
|
||
|
require_relative './name_checker'
|
||
|
require_relative '../permission'
|
||
|
require_relative './relator'
|
||
|
require_relative './like'
|
||
|
require_relative '../table/privacy_manager'
|
||
|
require_relative '../../../services/minimal-validation/validator'
|
||
|
require_relative '../../helpers/embed_redis_cache'
|
||
|
require_dependency 'cartodb/redis_vizjson_cache'
|
||
|
require_dependency 'carto/visualization'
|
||
|
|
||
|
# Every table has always at least one visualization (the "canonical visualization"), of type 'table',
|
||
|
# which shares the same privacy options as the table and gets synced.
|
||
|
# Users can create new visualizations, which will never be of type 'table',
|
||
|
# and those will use named maps when any source tables are private
|
||
|
module CartoDB
|
||
|
module Visualization
|
||
|
class Member
|
||
|
extend Forwardable
|
||
|
include Virtus.model
|
||
|
include CacheHelper
|
||
|
include Carto::VisualizationDependencies
|
||
|
|
||
|
PRIVACY_PUBLIC = 'public'.freeze # published and listable in public user profile
|
||
|
PRIVACY_PRIVATE = 'private'.freeze # not published (viz.json and embed_map should return 404)
|
||
|
PRIVACY_LINK = 'link'.freeze # published but not listen in public profile
|
||
|
PRIVACY_PROTECTED = 'password'.freeze # published but password protected
|
||
|
|
||
|
TYPE_CANONICAL = 'table'.freeze
|
||
|
TYPE_DERIVED = 'derived'.freeze
|
||
|
TYPE_SLIDE = 'slide'.freeze
|
||
|
TYPE_REMOTE = 'remote'.freeze
|
||
|
TYPE_KUVIZ = 'kuviz'.freeze
|
||
|
|
||
|
VALID_TYPES = [TYPE_CANONICAL, TYPE_DERIVED, TYPE_SLIDE, TYPE_REMOTE].freeze
|
||
|
|
||
|
KIND_GEOM = 'geom'.freeze
|
||
|
KIND_RASTER = 'raster'.freeze
|
||
|
|
||
|
PRIVACY_VALUES = [PRIVACY_PUBLIC, PRIVACY_PRIVATE, PRIVACY_LINK, PRIVACY_PROTECTED].freeze
|
||
|
TEMPLATE_NAME_PREFIX = 'tpl_'.freeze
|
||
|
|
||
|
PERMISSION_READONLY = CartoDB::Permission::ACCESS_READONLY
|
||
|
PERMISSION_READWRITE = CartoDB::Permission::ACCESS_READWRITE
|
||
|
|
||
|
TOKEN_DIGEST = '6da98b2da1b38c5ada2547ad2c3268caa1eb58dc20c9144ead844a2eda1917067a06dcb54833ba2'.freeze
|
||
|
|
||
|
VERSION_BUILDER = 3
|
||
|
|
||
|
DEFAULT_OPTIONS_VALUE = '{}'.freeze
|
||
|
|
||
|
# Upon adding new attributes modify also:
|
||
|
# services/data-repository/spec/unit/backend/sequel_spec.rb -> before do
|
||
|
# spec/support/helpers.rb -> random_attributes_for_vis_member
|
||
|
# app/models/visualization/presenter.rb
|
||
|
attribute :id, String
|
||
|
attribute :name, String
|
||
|
attribute :display_name, String
|
||
|
attribute :map_id, String
|
||
|
attribute :active_layer_id, String
|
||
|
attribute :type, String
|
||
|
attribute :privacy, String
|
||
|
attribute :tags, Array[String], default: []
|
||
|
attribute :description, String
|
||
|
attribute :license, String
|
||
|
attribute :source, String
|
||
|
attribute :attributions, String
|
||
|
attribute :title, String
|
||
|
attribute :created_at, Time
|
||
|
attribute :updated_at, Time
|
||
|
attribute :encrypted_password, String, default: nil
|
||
|
attribute :password_salt, String, default: nil
|
||
|
attribute :user_id, String
|
||
|
attribute :permission_id, String
|
||
|
attribute :locked, Boolean, default: false
|
||
|
attribute :parent_id, String, default: nil
|
||
|
attribute :kind, String, default: KIND_GEOM
|
||
|
attribute :prev_id, String, default: nil
|
||
|
attribute :next_id, String, default: nil
|
||
|
attribute :bbox, String, default: nil
|
||
|
attribute :auth_token, String, default: nil
|
||
|
attribute :version, Integer
|
||
|
# Don't use directly, use instead getter/setter "transition_options"
|
||
|
attribute :slide_transition_options, String, default: DEFAULT_OPTIONS_VALUE
|
||
|
attribute :active_child, String, default: nil
|
||
|
|
||
|
def_delegators :validator, :errors, :full_errors
|
||
|
def_delegators :relator, *Relator::INTERFACE
|
||
|
|
||
|
# This get called not only when creating a new but also when populating from the Collection
|
||
|
def initialize(attributes={}, repository=Visualization.repository, name_checker=nil)
|
||
|
super(attributes)
|
||
|
@repository = repository
|
||
|
self.id ||= @repository.next_id
|
||
|
@name_checker = name_checker
|
||
|
@validator = MinimalValidator::Validator.new
|
||
|
self.permission_change_valid = true # Changes upon set of different permission_id
|
||
|
# this flag is passed to the table in case of canonical visualizations. It's used to say to the table to not touch the database and only change the metadata information, useful for ghost tables
|
||
|
self.register_table_only = false
|
||
|
@redis_vizjson_cache = RedisVizjsonCache.new()
|
||
|
@old_privacy = @privacy
|
||
|
end
|
||
|
|
||
|
def self.remote_member(name, user_id, privacy, description, tags, license, source, attributions, display_name)
|
||
|
Member.new({
|
||
|
name: name,
|
||
|
user_id: user_id,
|
||
|
privacy: privacy,
|
||
|
description: description,
|
||
|
tags: tags,
|
||
|
license: license,
|
||
|
source: source,
|
||
|
attributions: attributions,
|
||
|
display_name: display_name,
|
||
|
type: TYPE_REMOTE})
|
||
|
end
|
||
|
|
||
|
def transition_options
|
||
|
::JSON.parse(self.slide_transition_options).symbolize_keys
|
||
|
end
|
||
|
|
||
|
def transition_options=(value)
|
||
|
self.slide_transition_options = ::JSON.dump(value.nil? ? DEFAULT_OPTIONS_VALUE : value)
|
||
|
end
|
||
|
|
||
|
def ==(other_vis)
|
||
|
self.id == other_vis.id
|
||
|
end
|
||
|
|
||
|
def default_privacy
|
||
|
can_be_private? ? PRIVACY_LINK : PRIVACY_PUBLIC
|
||
|
end
|
||
|
|
||
|
def store
|
||
|
raise CartoDB::InvalidMember.new(validator.errors) unless self.valid?
|
||
|
do_store
|
||
|
|
||
|
self
|
||
|
end
|
||
|
|
||
|
def store_from_map(fields)
|
||
|
self.map_id = fields[:map_id]
|
||
|
do_store(false)
|
||
|
self
|
||
|
end
|
||
|
|
||
|
def store_using_table(table_privacy_changed = false)
|
||
|
do_store(false, table_privacy_changed)
|
||
|
self
|
||
|
end
|
||
|
|
||
|
def valid?
|
||
|
validator.errors.store(:type, "Visualization type is not valid") unless valid_type?
|
||
|
validator.errors.store(:user, "Viewer users can't store visualizations") if user.viewer
|
||
|
|
||
|
validator.validate_presence_of(name: name, privacy: privacy, type: type, user_id: user_id)
|
||
|
validator.validate_in(:privacy, privacy, PRIVACY_VALUES)
|
||
|
# do not validate names for slides, it's never used
|
||
|
validator.validate_uniqueness_of(:name, available_name?) unless type_slide?
|
||
|
|
||
|
if privacy == PRIVACY_PROTECTED
|
||
|
validator.validate_presence_of_with_custom_message(
|
||
|
{ encrypted_password: encrypted_password },
|
||
|
"password can't be blank")
|
||
|
end
|
||
|
|
||
|
# Allow only "maintaining" privacy link for everyone but not setting it
|
||
|
if privacy == PRIVACY_LINK && privacy_changed
|
||
|
if derived?
|
||
|
validator.validate_expected_value(:private_maps_enabled, true, user.private_maps_enabled)
|
||
|
else
|
||
|
validator.validate_expected_value(:private_tables_enabled, true, user.private_tables_enabled)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if type_slide?
|
||
|
if parent_id.nil?
|
||
|
validator.errors.store(:parent_id, "Type #{TYPE_SLIDE} must have a parent") if parent_id.nil?
|
||
|
else
|
||
|
begin
|
||
|
parent_member = Member.new(id:parent_id).fetch
|
||
|
if parent_member.type != TYPE_DERIVED
|
||
|
validator.errors.store(:parent_id, "Type #{TYPE_SLIDE} must have parent of type #{TYPE_DERIVED}")
|
||
|
end
|
||
|
rescue KeyError
|
||
|
validator.errors.store(:parent_id, "Type #{TYPE_SLIDE} has non-existing parent id")
|
||
|
end
|
||
|
end
|
||
|
else
|
||
|
validator.errors.store(:parent_id, "Type #{type} must not have parent") unless parent_id.nil?
|
||
|
end
|
||
|
|
||
|
unless permission_id.nil?
|
||
|
validator.errors.store(:permission_id, 'Cannot modify permission') unless permission_change_valid
|
||
|
end
|
||
|
|
||
|
if !license.nil? && !license.empty? && Carto::License.find(license.to_sym).nil?
|
||
|
validator.errors.store(:license, 'License should be an empty or a valid value')
|
||
|
end
|
||
|
|
||
|
validator.valid?
|
||
|
end
|
||
|
|
||
|
def valid_type?
|
||
|
VALID_TYPES.include?(type)
|
||
|
end
|
||
|
|
||
|
def fetch
|
||
|
data = repository.fetch(id)
|
||
|
raise KeyError if data.nil?
|
||
|
self.attributes = data
|
||
|
self.name_changed = false
|
||
|
@old_privacy = @privacy
|
||
|
self.privacy_changed = false
|
||
|
self.permission_change_valid = true
|
||
|
self.dirty = false
|
||
|
validator.reset
|
||
|
self
|
||
|
end
|
||
|
|
||
|
def delete_from_table
|
||
|
delete(true)
|
||
|
end
|
||
|
|
||
|
def delete(from_table_deletion = false)
|
||
|
raise CartoDB::InvalidMember.new(user: "Viewer users can't delete visualizations") if user.viewer
|
||
|
|
||
|
repository.transaction do
|
||
|
unlink_self_from_list!
|
||
|
|
||
|
support_tables.delete_all
|
||
|
|
||
|
overlays.map(&:destroy)
|
||
|
safe_sequel_delete do
|
||
|
# "Mark" that this vis id is the destructor to avoid cycles: Vis -> Map -> relatedvis (Vis again)
|
||
|
related_map = map
|
||
|
related_map.being_destroyed_by_vis_id = id
|
||
|
related_map.destroy
|
||
|
end if map
|
||
|
safe_sequel_delete { table.destroy } if type == TYPE_CANONICAL && table && !from_table_deletion
|
||
|
safe_sequel_delete do
|
||
|
children.map do |child|
|
||
|
# Refetch each item before removal so Relator reloads prev/next cursors
|
||
|
child.fetch.delete
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Avoid invalidating if the visualization has already been destroyed
|
||
|
# This happens deleting a canonical visualization, which triggers a table deletion,
|
||
|
# which triggers a second deletion of the same visualization
|
||
|
carto_vis = carto_visualization
|
||
|
if carto_vis
|
||
|
Carto::NamedMaps::Api.new(carto_vis).destroy
|
||
|
invalidate_cache
|
||
|
end
|
||
|
|
||
|
safe_sequel_delete { permission.destroy_shared_entities } if permission
|
||
|
safe_sequel_delete { repository.delete(id) }
|
||
|
safe_sequel_delete { permission.destroy } if permission
|
||
|
attributes.keys.each { |key| send("#{key}=", nil) }
|
||
|
end
|
||
|
|
||
|
self
|
||
|
end
|
||
|
|
||
|
# A visualization is linked to a table when it uses that table in a layergroup (but is not the canonical table)
|
||
|
def unlink_from(table)
|
||
|
invalidate_cache
|
||
|
remove_layers_from(table)
|
||
|
end
|
||
|
|
||
|
def name=(name)
|
||
|
name = name.downcase if name && table?
|
||
|
self.name_changed = true if name != @name && !@name.nil?
|
||
|
self.old_name = @name
|
||
|
super(name)
|
||
|
end
|
||
|
|
||
|
def description=(description)
|
||
|
self.dirty = true if description != @description && !@description.nil?
|
||
|
super(description)
|
||
|
end
|
||
|
|
||
|
def attributions=(value)
|
||
|
self.dirty = true if value != @attributions
|
||
|
self.attributions_changed = true if value != @attributions
|
||
|
super(value)
|
||
|
end
|
||
|
|
||
|
def permission_id=(permission_id)
|
||
|
self.permission_change_valid = false
|
||
|
self.permission_change_valid = true if (@permission_id.nil? || @permission_id == permission_id)
|
||
|
super(permission_id)
|
||
|
end
|
||
|
|
||
|
def privacy=(new_privacy)
|
||
|
new_privacy = new_privacy.downcase if new_privacy
|
||
|
if new_privacy != @privacy && !@privacy.nil?
|
||
|
self.privacy_changed = true
|
||
|
@old_privacy = @privacy
|
||
|
end
|
||
|
super(new_privacy)
|
||
|
end
|
||
|
|
||
|
def tags=(tags)
|
||
|
tags.reject!(&:blank?) if tags
|
||
|
super(tags)
|
||
|
end
|
||
|
|
||
|
def version=(version)
|
||
|
self.dirty = true
|
||
|
super(version)
|
||
|
end
|
||
|
|
||
|
def public?
|
||
|
privacy == PRIVACY_PUBLIC
|
||
|
end
|
||
|
|
||
|
def public_with_link?
|
||
|
privacy == PRIVACY_LINK
|
||
|
end
|
||
|
|
||
|
def private?
|
||
|
privacy == PRIVACY_PRIVATE and not organization?
|
||
|
end
|
||
|
|
||
|
def is_privacy_private?
|
||
|
privacy == PRIVACY_PRIVATE
|
||
|
end
|
||
|
|
||
|
def can_be_private?(owner = user)
|
||
|
derived? ? owner.try(:private_maps_enabled) : owner.try(:private_tables_enabled)
|
||
|
end
|
||
|
|
||
|
def organization?
|
||
|
privacy == PRIVACY_PRIVATE and permission.acl.size > 0
|
||
|
end
|
||
|
|
||
|
def password_protected?
|
||
|
privacy == PRIVACY_PROTECTED
|
||
|
end
|
||
|
|
||
|
# Called by controllers upon rendering
|
||
|
def to_json(options={})
|
||
|
::JSON.dump(to_hash(options))
|
||
|
end
|
||
|
|
||
|
def to_hash(options={})
|
||
|
presenter = Presenter.new(self, options.merge(real_privacy: true))
|
||
|
options.delete(:public_fields_only) === true ? presenter.to_public_poro : presenter.to_poro
|
||
|
end
|
||
|
|
||
|
def to_vizjson(options={})
|
||
|
@redis_vizjson_cache.cached(id, options.fetch(:https_request, false)) do
|
||
|
calculate_vizjson(options)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def is_owner?(user)
|
||
|
user && user.id == user_id
|
||
|
end
|
||
|
|
||
|
# @param user ::User
|
||
|
# @param permission_type String PERMISSION_xxx
|
||
|
def has_permission?(user, permission_type)
|
||
|
return false if user.viewer && permission_type == PERMISSION_READWRITE
|
||
|
return is_owner?(user) if permission_id.nil?
|
||
|
is_owner?(user) || permission.permitted?(user, permission_type)
|
||
|
end
|
||
|
|
||
|
def can_copy?(user)
|
||
|
!raster_kind? && has_permission?(user, PERMISSION_READONLY)
|
||
|
end
|
||
|
|
||
|
def raster_kind?
|
||
|
kind == KIND_RASTER
|
||
|
end
|
||
|
|
||
|
def users_with_permissions(permission_types)
|
||
|
permission.users_with_permissions(permission_types)
|
||
|
end
|
||
|
|
||
|
def varnish_key
|
||
|
sorted_table_names = related_tables.map{ |table|
|
||
|
"#{user.database_schema}.#{table.name}"
|
||
|
}.sort { |i, j|
|
||
|
i <=> j
|
||
|
}.join(',')
|
||
|
"#{user.database_name}:#{sorted_table_names},#{id}"
|
||
|
end
|
||
|
|
||
|
def surrogate_key
|
||
|
get_surrogate_key(CartoDB::SURROGATE_NAMESPACE_VISUALIZATION, self.id)
|
||
|
end
|
||
|
|
||
|
def varnish_vizjson_key
|
||
|
".*#{id}:vizjson"
|
||
|
end
|
||
|
|
||
|
def derived?
|
||
|
type == TYPE_DERIVED
|
||
|
end
|
||
|
|
||
|
def table?
|
||
|
type == TYPE_CANONICAL
|
||
|
end
|
||
|
# Used at Carto::Api::VisualizationPresenter
|
||
|
alias :canonical? :table?
|
||
|
|
||
|
def type_slide?
|
||
|
type == TYPE_SLIDE
|
||
|
end
|
||
|
|
||
|
def kuviz?
|
||
|
type == TYPE_KUVIZ
|
||
|
end
|
||
|
|
||
|
def invalidate_cache
|
||
|
invalidate_redis_cache
|
||
|
invalidate_varnish_vizjson_cache
|
||
|
|
||
|
parent.invalidate_cache unless parent_id.nil?
|
||
|
end
|
||
|
|
||
|
def has_private_tables?
|
||
|
has_private_tables = false
|
||
|
related_tables.each { |table|
|
||
|
has_private_tables |= table.private?
|
||
|
}
|
||
|
has_private_tables
|
||
|
end
|
||
|
|
||
|
# Despite storing always a named map, no need to retrieve it for "public" visualizations
|
||
|
def retrieve_named_map?
|
||
|
password_protected? || has_private_tables?
|
||
|
end
|
||
|
|
||
|
def password=(value)
|
||
|
if value && value.size > 0
|
||
|
@password_salt = ""
|
||
|
@encrypted_password = Carto::Common::EncryptionService.encrypt(password: value,
|
||
|
secret: Cartodb.config[:password_secret])
|
||
|
self.dirty = true
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def has_password?
|
||
|
( !@password_salt.nil? && !@encrypted_password.nil? )
|
||
|
end
|
||
|
|
||
|
def password_valid?(password)
|
||
|
Carto::Common::EncryptionService.verify(password: password, secure_password: @encrypted_password,
|
||
|
salt: @password_salt, secret: Cartodb.config[:password_secret])
|
||
|
end
|
||
|
|
||
|
def remove_password
|
||
|
@password_salt = nil
|
||
|
@encrypted_password = nil
|
||
|
end
|
||
|
|
||
|
# To be stored with the named map
|
||
|
def make_auth_token
|
||
|
Carto::Common::EncryptionService.make_token(length: 64)
|
||
|
end
|
||
|
|
||
|
def get_auth_token
|
||
|
if auth_token.nil?
|
||
|
auth_token = make_auth_token
|
||
|
store
|
||
|
end
|
||
|
auth_token
|
||
|
end
|
||
|
|
||
|
def get_auth_tokens
|
||
|
[get_auth_token]
|
||
|
end
|
||
|
|
||
|
def supports_private_maps?
|
||
|
!user.nil? && user.private_maps_enabled?
|
||
|
end
|
||
|
|
||
|
def published?
|
||
|
!is_privacy_private? && (!builder? || !derived? || mapcapped?)
|
||
|
end
|
||
|
|
||
|
def builder?
|
||
|
version == VERSION_BUILDER
|
||
|
end
|
||
|
|
||
|
# @param other_vis CartoDB::Visualization::Member|nil
|
||
|
# Note: Changes state both of self, other_vis and other affected list items, but only reloads self & other_vis
|
||
|
def set_next_list_item!(other_vis)
|
||
|
repository.transaction do
|
||
|
close_list_gap(other_vis)
|
||
|
|
||
|
# Now insert other_vis after self
|
||
|
unless other_vis.nil?
|
||
|
if self.next_id.nil?
|
||
|
other_vis.next_id = nil
|
||
|
else
|
||
|
other_vis.next_id = self.next_id
|
||
|
next_item = next_list_item
|
||
|
next_item.prev_id = other_vis.id
|
||
|
next_item.store
|
||
|
end
|
||
|
self.next_id = other_vis.id
|
||
|
other_vis.prev_id = self.id
|
||
|
other_vis.store
|
||
|
.fetch
|
||
|
end
|
||
|
|
||
|
store
|
||
|
end
|
||
|
|
||
|
fetch
|
||
|
end
|
||
|
|
||
|
# @param other_vis CartoDB::Visualization::Member|nil
|
||
|
# Note: Changes state both of self, other_vis and other affected list items, but only reloads self & other_vis
|
||
|
def set_prev_list_item!(other_vis)
|
||
|
repository.transaction do
|
||
|
close_list_gap(other_vis)
|
||
|
|
||
|
# Now insert other_vis after self
|
||
|
unless other_vis.nil?
|
||
|
if self.prev_id.nil?
|
||
|
other_vis.prev_id = nil
|
||
|
else
|
||
|
other_vis.prev_id = self.prev_id
|
||
|
prev_item = prev_list_item
|
||
|
prev_item.next_id = other_vis.id
|
||
|
prev_item.store
|
||
|
end
|
||
|
self.prev_id = other_vis.id
|
||
|
other_vis.next_id = self.id
|
||
|
other_vis.store
|
||
|
.fetch
|
||
|
end
|
||
|
|
||
|
store
|
||
|
end
|
||
|
fetch
|
||
|
end
|
||
|
|
||
|
def unlink_self_from_list!
|
||
|
repository.transaction do
|
||
|
unless self.prev_id.nil?
|
||
|
prev_item = prev_list_item
|
||
|
prev_item.next_id = self.next_id
|
||
|
prev_item.store
|
||
|
end
|
||
|
unless self.next_id.nil?
|
||
|
next_item = next_list_item
|
||
|
next_item.prev_id = self.prev_id
|
||
|
next_item.store
|
||
|
end
|
||
|
self.prev_id = nil
|
||
|
self.next_id = nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def liked_by?(user)
|
||
|
!likes.select { |like| like.actor == user.id }.first.nil?
|
||
|
end
|
||
|
|
||
|
# @param viewer_user ::User
|
||
|
def qualified_name(viewer_user=nil)
|
||
|
if viewer_user.nil? || is_owner?(viewer_user)
|
||
|
name
|
||
|
else
|
||
|
"#{user.sql_safe_database_schema}.#{name}"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
attr_accessor :register_table_only
|
||
|
|
||
|
def invalidate_redis_cache
|
||
|
@redis_vizjson_cache.invalidate(id)
|
||
|
embed_redis_cache.invalidate(self.id)
|
||
|
end
|
||
|
|
||
|
def save_named_map
|
||
|
return if type == TYPE_REMOTE
|
||
|
return true if named_map_updates_disabled?
|
||
|
|
||
|
unless @updating_named_maps
|
||
|
SequelRails.connection.after_commit do
|
||
|
@updating_named_maps = false
|
||
|
(get_named_map ? update_named_map : create_named_map) if carto_visualization
|
||
|
end
|
||
|
@updating_named_maps = true
|
||
|
end
|
||
|
true
|
||
|
end
|
||
|
|
||
|
def get_named_map
|
||
|
return false if type == TYPE_REMOTE
|
||
|
|
||
|
Carto::NamedMaps::Api.new(carto_visualization).show if carto_visualization
|
||
|
end
|
||
|
|
||
|
def license_info
|
||
|
if !license.nil?
|
||
|
Carto::License.find(license.to_sym)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def attributions_from_derived_visualizations
|
||
|
related_canonical_visualizations.map(&:attributions).reject {|attribution| attribution.blank?}
|
||
|
end
|
||
|
|
||
|
def map
|
||
|
@map ||= ::Map.where(id: map_id).first
|
||
|
end
|
||
|
|
||
|
def mapcaps
|
||
|
Carto::Mapcap.latest_for_visualization(id)
|
||
|
end
|
||
|
|
||
|
def latest_mapcap
|
||
|
mapcaps.first
|
||
|
end
|
||
|
|
||
|
def mapcapped?
|
||
|
mapcaps.exists?
|
||
|
end
|
||
|
|
||
|
def invalidate_for_permissions_change
|
||
|
# A change in permissions should trigger the same invalidations as a privacy change
|
||
|
self.privacy_changed = true
|
||
|
invalidate_cache
|
||
|
save_named_map
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
attr_reader :repository, :name_checker, :validator
|
||
|
attr_accessor :privacy_changed, :name_changed, :old_name, :permission_change_valid, :dirty, :attributions_changed
|
||
|
|
||
|
def named_map_updates_disabled?
|
||
|
mapcapped? && !privacy_changed
|
||
|
end
|
||
|
|
||
|
def embed_redis_cache
|
||
|
@embed_redis_cache ||= EmbedRedisCache.new($tables_metadata)
|
||
|
end
|
||
|
|
||
|
def calculate_vizjson(options={})
|
||
|
vizjson_options = {
|
||
|
full: false,
|
||
|
user_name: user.username,
|
||
|
user_api_key: user.api_key,
|
||
|
user: user,
|
||
|
viewer_user: user
|
||
|
}.merge(options)
|
||
|
VizJSON.new(self, vizjson_options, configuration).to_poro
|
||
|
end
|
||
|
|
||
|
def invalidate_varnish_vizjson_cache
|
||
|
CartoDB::Varnish.new.purge(varnish_vizjson_key)
|
||
|
end
|
||
|
|
||
|
def close_list_gap(other_vis)
|
||
|
reload_self = false
|
||
|
|
||
|
if other_vis.nil?
|
||
|
self.next_id = nil
|
||
|
old_prev = nil
|
||
|
old_next = nil
|
||
|
else
|
||
|
old_prev = other_vis.prev_list_item
|
||
|
old_next = other_vis.next_list_item
|
||
|
end
|
||
|
|
||
|
# First close gap left by other_vis
|
||
|
unless old_prev.nil?
|
||
|
old_prev.next_id = old_next.nil? ? nil : old_next.id
|
||
|
old_prev.store
|
||
|
reload_self |= old_prev.id == self.id
|
||
|
end
|
||
|
unless old_next.nil?
|
||
|
old_next.prev_id = old_prev.nil? ? nil : old_prev.id
|
||
|
old_next.store
|
||
|
reload_self |= old_next.id == self.id
|
||
|
end
|
||
|
|
||
|
fetch if reload_self
|
||
|
end
|
||
|
|
||
|
def do_store(propagate_changes = true, table_privacy_changed = false)
|
||
|
self.version = user.new_visualizations_version if version.nil?
|
||
|
|
||
|
if password_protected?
|
||
|
raise CartoDB::InvalidMember.new('No password set and required') unless has_password?
|
||
|
else
|
||
|
remove_password
|
||
|
end
|
||
|
|
||
|
# Warning: imports create by default private canonical visualizations
|
||
|
if type != TYPE_CANONICAL && @privacy == PRIVACY_PRIVATE && privacy_changed && !supports_private_maps?
|
||
|
raise CartoDB::InvalidMember
|
||
|
end
|
||
|
|
||
|
perform_invalidations(table_privacy_changed)
|
||
|
|
||
|
set_timestamps
|
||
|
|
||
|
# Ensure a permission is set before saving the visualization
|
||
|
if permission.nil?
|
||
|
perm = CartoDB::Permission.new
|
||
|
perm.owner = user
|
||
|
perm.save
|
||
|
@permission_id = perm.id
|
||
|
end
|
||
|
repository.store(id, attributes.to_hash)
|
||
|
|
||
|
restore_previous_privacy unless save_named_map
|
||
|
|
||
|
propagate_attribution_change if table
|
||
|
if type == TYPE_REMOTE || type == TYPE_CANONICAL
|
||
|
propagate_privacy_and_name_to(table) if table and propagate_changes
|
||
|
else
|
||
|
propagate_name_to(table) if table and propagate_changes
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def restore_previous_privacy
|
||
|
unless @old_privacy.nil?
|
||
|
self.privacy = @old_privacy
|
||
|
attributes[:privacy] = @old_privacy
|
||
|
repository.store(id, attributes.to_hash)
|
||
|
end
|
||
|
rescue => exception
|
||
|
CartoDB.notify_exception(exception, user: user, message: "Error restoring previous visualization privacy")
|
||
|
raise exception
|
||
|
end
|
||
|
|
||
|
def perform_invalidations(table_privacy_changed)
|
||
|
# previously we used 'invalidate_cache' but due to public_map displaying all the user public visualizations,
|
||
|
# now we need to purgue everything to avoid cached stale data or public->priv still showing scenarios
|
||
|
if name_changed || privacy_changed || table_privacy_changed || dirty
|
||
|
invalidate_cache
|
||
|
end
|
||
|
|
||
|
# When a table's relevant data is changed, propagate to all who use it or relate to it
|
||
|
if dirty && table
|
||
|
table.affected_visualizations.each do |affected_vis|
|
||
|
affected_vis.invalidate_cache
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def create_named_map
|
||
|
return unless map
|
||
|
Carto::NamedMaps::Api.new(carto_visualization).create
|
||
|
end
|
||
|
|
||
|
def update_named_map
|
||
|
return if named_map_updates_disabled? || map.nil?
|
||
|
|
||
|
# A visualization destroy triggers destroys on all its layers. Each
|
||
|
# layer destroy, will trigger an update back to the visualization. When
|
||
|
# the last layer is destroyed, and the visualization named map template
|
||
|
# is generated to be updated, it will contain no layers, causing an
|
||
|
# error at the Maps API. This is a hack to prevent that update and error
|
||
|
# from happening. A better way to solve this would be to get
|
||
|
# callbacks under control.
|
||
|
presentation_visualization = carto_visualization.try(:for_presentation)
|
||
|
if presentation_visualization && presentation_visualization.layers.any?
|
||
|
Carto::NamedMaps::Api.new(presentation_visualization).update
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def propagate_privacy_and_name_to(table)
|
||
|
raise "Empty table sent to Visualization::Member propagate_privacy_and_name_to()" unless table
|
||
|
propagate_privacy_to(table) if privacy_changed
|
||
|
propagate_name_to(table) if name_changed
|
||
|
end
|
||
|
|
||
|
def propagate_privacy_to(table)
|
||
|
if type == TYPE_CANONICAL
|
||
|
CartoDB::TablePrivacyManager.new(table)
|
||
|
.set_from_visualization(self)
|
||
|
.update_cdb_tablemetadata
|
||
|
end
|
||
|
self
|
||
|
end
|
||
|
|
||
|
# @param table Table
|
||
|
def propagate_name_to(table)
|
||
|
table.register_table_only = register_table_only
|
||
|
table.name = name
|
||
|
table.update(name: name)
|
||
|
if name_changed
|
||
|
support_tables.rename(old_name, name, recreate_constraints=true, seek_parent_name=old_name)
|
||
|
end
|
||
|
self
|
||
|
rescue => exception
|
||
|
if name_changed && !(exception.to_s =~ /relation.*does not exist/)
|
||
|
revert_name_change(old_name)
|
||
|
end
|
||
|
raise CartoDB::InvalidMember.new(exception.to_s)
|
||
|
end
|
||
|
|
||
|
def propagate_attribution_change
|
||
|
return unless attributions_changed
|
||
|
|
||
|
table.propagate_attribution_change(attributions)
|
||
|
end
|
||
|
|
||
|
def revert_name_change(previous_name)
|
||
|
self.name = previous_name
|
||
|
store
|
||
|
rescue => exception
|
||
|
raise CartoDB::InvalidMember.new(exception.to_s)
|
||
|
end
|
||
|
|
||
|
def set_timestamps
|
||
|
self.created_at ||= Time.now
|
||
|
self.updated_at = Time.now
|
||
|
self
|
||
|
end
|
||
|
|
||
|
def relator
|
||
|
Relator.new(map, attributes)
|
||
|
end
|
||
|
|
||
|
def name_checker
|
||
|
@name_checker || NameChecker.new(user)
|
||
|
end
|
||
|
|
||
|
def available_name?
|
||
|
return true unless user && name_changed
|
||
|
name_checker.available?(name)
|
||
|
end
|
||
|
|
||
|
def remove_layers_from(table)
|
||
|
related_layers_from(table).each do |layer|
|
||
|
# Using delete to avoid hooks, as they generate a conflict between ORMs and are
|
||
|
# not needed in this case since they are already triggered by deleting the layer
|
||
|
Carto::Analysis.find_by_natural_id(id, layer.source_id).try(:delete) if layer.source_id
|
||
|
|
||
|
map.remove_layer(layer)
|
||
|
layer.destroy
|
||
|
end
|
||
|
self.active_layer_id = layers(:cartodb).first.nil? ? nil : layers(:cartodb).first.id
|
||
|
store
|
||
|
end
|
||
|
|
||
|
def related_layers_from(table)
|
||
|
layers(:cartodb).select do |layer|
|
||
|
(layer.user_tables.map(&:name) + [layer.options.fetch('table_name', nil)]).include?(table.name)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def configuration
|
||
|
return {} unless defined?(Cartodb)
|
||
|
Cartodb.config
|
||
|
end
|
||
|
|
||
|
def safe_sequel_delete
|
||
|
yield
|
||
|
rescue Sequel::NoExistingObject => exception
|
||
|
# INFO: don't fail on nonexistant object delete
|
||
|
CartoDB.notify_exception(exception)
|
||
|
end
|
||
|
|
||
|
def carto_visualization
|
||
|
Carto::Visualization.where(id: id).first
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|