880 lines
23 KiB
Ruby
880 lines
23 KiB
Ruby
|
require 'active_record'
|
||
|
require 'cartodb-common'
|
||
|
require_relative '../visualization/stats'
|
||
|
require_relative '../quota_checker'
|
||
|
require_dependency 'carto/named_maps/api'
|
||
|
require_dependency 'carto/helpers/auth_token_generator'
|
||
|
require_dependency 'carto/uuidhelper'
|
||
|
require_dependency 'carto/visualization_invalidation_service'
|
||
|
require_dependency 'carto/visualization_backup_service'
|
||
|
|
||
|
module Carto::VisualizationDependencies
|
||
|
def fully_dependent_on?(user_table)
|
||
|
derived? && layers_dependent_on(user_table).count == data_layers.count
|
||
|
end
|
||
|
|
||
|
def partially_dependent_on?(user_table)
|
||
|
derived? && layers_dependent_on(user_table).count.between?(1, data_layers.count - 1)
|
||
|
end
|
||
|
|
||
|
def dependent_on?(user_table)
|
||
|
derived? && layers_dependent_on(user_table).any?
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def layers_dependent_on(user_table)
|
||
|
data_layers.select { |l| l.depends_on?(user_table) }
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class Carto::Visualization < ActiveRecord::Base
|
||
|
include CacheHelper
|
||
|
include Carto::UUIDHelper
|
||
|
include Carto::AuthTokenGenerator
|
||
|
include Carto::VisualizationDependencies
|
||
|
include Carto::VisualizationBackupService
|
||
|
|
||
|
TYPE_CANONICAL = 'table'.freeze
|
||
|
TYPE_DERIVED = 'derived'.freeze
|
||
|
TYPE_SLIDE = 'slide'.freeze
|
||
|
TYPE_REMOTE = 'remote'.freeze
|
||
|
TYPE_KUVIZ = 'kuviz'.freeze
|
||
|
TYPE_APP = 'app'.freeze
|
||
|
|
||
|
VALID_TYPES = [TYPE_CANONICAL, TYPE_DERIVED, TYPE_SLIDE, TYPE_REMOTE, TYPE_KUVIZ, TYPE_APP].freeze
|
||
|
MAP_TYPES = [TYPE_DERIVED, TYPE_KUVIZ].freeze
|
||
|
|
||
|
KIND_GEOM = 'geom'.freeze
|
||
|
KIND_RASTER = 'raster'.freeze
|
||
|
|
||
|
PRIVACY_PUBLIC = 'public'.freeze
|
||
|
PRIVACY_PRIVATE = 'private'.freeze
|
||
|
PRIVACY_LINK = 'link'.freeze
|
||
|
PRIVACY_PROTECTED = 'password'.freeze
|
||
|
PRIVACIES = [PRIVACY_LINK, PRIVACY_PROTECTED, PRIVACY_PUBLIC, PRIVACY_PRIVATE].freeze
|
||
|
|
||
|
VERSION_BUILDER = 3
|
||
|
|
||
|
V2_VISUALIZATIONS_REDIS_KEY = 'vizjson2_visualizations'.freeze
|
||
|
|
||
|
scope :remotes, -> { where(type: TYPE_REMOTE) }
|
||
|
|
||
|
# INFO: disable ActiveRecord inheritance column
|
||
|
self.inheritance_column = :_type
|
||
|
|
||
|
belongs_to :user, -> { select(Carto::User::DEFAULT_SELECT) }, inverse_of: :visualizations
|
||
|
belongs_to :full_user, -> { readonly(true) }, class_name: Carto::User, inverse_of: :visualizations,
|
||
|
primary_key: :id, foreign_key: :user_id
|
||
|
|
||
|
belongs_to :permission, inverse_of: :visualization, dependent: :destroy
|
||
|
|
||
|
has_many :likes, foreign_key: :subject
|
||
|
has_many :shared_entities, foreign_key: :entity_id, inverse_of: :visualization, dependent: :destroy
|
||
|
|
||
|
has_one :external_source, class_name: Carto::ExternalSource, dependent: :destroy, inverse_of: :visualization
|
||
|
has_many :unordered_children, class_name: Carto::Visualization, foreign_key: :parent_id
|
||
|
|
||
|
has_many :overlays, -> { order(:order) }, dependent: :destroy, inverse_of: :visualization
|
||
|
|
||
|
belongs_to :active_layer, class_name: Carto::Layer
|
||
|
|
||
|
belongs_to :map, class_name: Carto::Map, inverse_of: :visualization, dependent: :destroy
|
||
|
|
||
|
has_one :asset, class_name: Carto::Asset, inverse_of: :visualization, dependent: :destroy
|
||
|
|
||
|
has_many :related_templates, class_name: Carto::Template, foreign_key: :source_visualization_id
|
||
|
|
||
|
has_one :synchronization, class_name: Carto::Synchronization, dependent: :destroy
|
||
|
has_many :external_sources, class_name: Carto::ExternalSource
|
||
|
|
||
|
has_many :analyses, class_name: Carto::Analysis
|
||
|
has_many :mapcaps, -> { order('created_at DESC') }, class_name: Carto::Mapcap, dependent: :destroy
|
||
|
|
||
|
has_one :state, class_name: Carto::State, autosave: true
|
||
|
|
||
|
has_many :snapshots, class_name: Carto::Snapshot, dependent: :destroy
|
||
|
|
||
|
validates :name, :privacy, :type, :user_id, :version, presence: true
|
||
|
validates :privacy, inclusion: { in: PRIVACIES }
|
||
|
validates :type, inclusion: { in: VALID_TYPES }
|
||
|
validates :name, uniqueness: { scope: [:user_id, :type] }, if: -> { kuviz? || app? }
|
||
|
validate :validate_password_presence
|
||
|
validate :validate_privacy_changes
|
||
|
validate :validate_user_not_viewer, on: :create
|
||
|
|
||
|
before_validation :set_default_version, :set_register_table_only
|
||
|
before_create :set_random_id, :set_default_permission
|
||
|
|
||
|
before_save :remove_password_if_unprotected
|
||
|
after_save :propagate_attribution_change
|
||
|
after_save :propagate_privacy_and_name_to, if: :table
|
||
|
|
||
|
before_destroy :backup_visualization
|
||
|
after_commit :perform_invalidations
|
||
|
|
||
|
attr_accessor :register_table_only
|
||
|
|
||
|
def set_register_table_only
|
||
|
self.register_table_only = false
|
||
|
# This is a callback, returning `true` avoids halting because of assignment `false` return value
|
||
|
true
|
||
|
end
|
||
|
|
||
|
def set_default_version
|
||
|
self.version ||= user.try(:new_visualizations_version)
|
||
|
end
|
||
|
|
||
|
DELETED_COLUMNS = ['state_id', 'url_options'].freeze
|
||
|
|
||
|
def self.columns
|
||
|
super.reject { |c| DELETED_COLUMNS.include?(c.name) }
|
||
|
end
|
||
|
|
||
|
def size
|
||
|
# Only canonical visualizations (Datasets) have a related table and then count against disk quota,
|
||
|
# but we want to not break and even allow ordering by size multiple types
|
||
|
if user_table
|
||
|
user_table.size
|
||
|
elsif remote? && external_source
|
||
|
external_source.size
|
||
|
else
|
||
|
0
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def tags
|
||
|
tags = super
|
||
|
tags == nil ? [] : tags
|
||
|
end
|
||
|
|
||
|
def tags=(tags)
|
||
|
tags.reject!(&:blank?) if tags
|
||
|
super(tags)
|
||
|
end
|
||
|
|
||
|
def user_table
|
||
|
map.user_table if map
|
||
|
end
|
||
|
|
||
|
def table
|
||
|
@table ||= user_table.try(:service)
|
||
|
end
|
||
|
|
||
|
def layers_with_data_readable_by(user)
|
||
|
return [] unless map
|
||
|
map.layers.select { |l| l.data_readable_by?(user) }
|
||
|
end
|
||
|
|
||
|
def related_tables
|
||
|
@related_tables ||= get_related_tables
|
||
|
end
|
||
|
|
||
|
def related_tables_readable_by(user)
|
||
|
layers_with_data_readable_by(user).map { |l| l.user_tables_readable_by(user) }.flatten.uniq
|
||
|
end
|
||
|
|
||
|
def related_canonical_visualizations
|
||
|
@related_canonical_visualizations ||= get_related_canonical_visualizations
|
||
|
end
|
||
|
|
||
|
def stats
|
||
|
@stats ||= CartoDB::Visualization::Stats.new(self).to_poro
|
||
|
end
|
||
|
|
||
|
def transition_options
|
||
|
@transition_options ||= (slide_transition_options.nil? ? {} : JSON.parse(slide_transition_options).symbolize_keys)
|
||
|
end
|
||
|
|
||
|
def transition_options=(value)
|
||
|
self.slide_transition_options = ::JSON.dump(value.nil? ? DEFAULT_OPTIONS_VALUE : value)
|
||
|
end
|
||
|
|
||
|
def children
|
||
|
ordered = []
|
||
|
children_vis = self.unordered_children
|
||
|
if children_vis.count > 0
|
||
|
ordered << children_vis.select { |vis| vis.prev_id.nil? }.first
|
||
|
while !ordered.last.next_id.nil?
|
||
|
target = ordered.last.next_id
|
||
|
unless target.nil?
|
||
|
ordered << children_vis.select { |vis| vis.id == target }.first
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
ordered
|
||
|
end
|
||
|
|
||
|
# TODO: refactor next methods, all have similar naming but some receive user and some others user_id
|
||
|
def liked_by?(user)
|
||
|
likes_by_user(user).any?
|
||
|
end
|
||
|
|
||
|
def likes_by_user(user)
|
||
|
likes.where(actor: user.id)
|
||
|
end
|
||
|
|
||
|
def add_like_from(user)
|
||
|
unless has_read_permission?(user)
|
||
|
raise UnauthorizedLikeError
|
||
|
end
|
||
|
|
||
|
likes.create!(actor: user.id)
|
||
|
|
||
|
self
|
||
|
rescue ActiveRecord::RecordNotUnique
|
||
|
raise AlreadyLikedError
|
||
|
end
|
||
|
|
||
|
def remove_like_from(user)
|
||
|
unless has_read_permission?(user)
|
||
|
raise UnauthorizedLikeError
|
||
|
end
|
||
|
|
||
|
item = likes.where(actor: user.id)
|
||
|
item.first.destroy unless item.first.nil?
|
||
|
|
||
|
self
|
||
|
end
|
||
|
|
||
|
def send_like_email(current_viewer, vis_preview_image)
|
||
|
if self.type == Carto::Visualization::TYPE_CANONICAL
|
||
|
::Resque.enqueue(::Resque::UserJobs::Mail::TableLiked, self.id, current_viewer.id, vis_preview_image)
|
||
|
elsif self.type == Carto::Visualization::TYPE_DERIVED
|
||
|
::Resque.enqueue(::Resque::UserJobs::Mail::MapLiked, self.id, current_viewer.id, vis_preview_image)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def is_viewable_by_user?(user)
|
||
|
is_publically_accesible? || has_read_permission?(user)
|
||
|
end
|
||
|
|
||
|
def is_accesible_by_user?(user)
|
||
|
is_viewable_by_user?(user) || password_protected?
|
||
|
end
|
||
|
|
||
|
def is_accessible_with_password?(user, password)
|
||
|
is_viewable_by_user?(user) || password_valid?(password)
|
||
|
end
|
||
|
|
||
|
def is_publically_accesible?
|
||
|
(public? || public_with_link?) && published?
|
||
|
end
|
||
|
|
||
|
def writable_by?(user)
|
||
|
(user_id == user.id && !user.viewer?) || has_write_permission?(user)
|
||
|
end
|
||
|
|
||
|
def varnish_key
|
||
|
"#{user.database_name}:#{sorted_related_table_names},#{id}"
|
||
|
end
|
||
|
|
||
|
def surrogate_key
|
||
|
get_surrogate_key(CartoDB::SURROGATE_NAMESPACE_VISUALIZATION, id)
|
||
|
end
|
||
|
|
||
|
def qualified_name(viewer_user = nil)
|
||
|
if viewer_user.nil? || owner?(viewer_user)
|
||
|
name
|
||
|
else
|
||
|
"#{user.sql_safe_database_schema}.#{name}"
|
||
|
end
|
||
|
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 has_password?
|
||
|
!password_salt.nil? && !encrypted_password.nil?
|
||
|
end
|
||
|
|
||
|
def type_slide?
|
||
|
type == TYPE_SLIDE
|
||
|
end
|
||
|
|
||
|
def kind_raster?
|
||
|
kind == KIND_RASTER
|
||
|
end
|
||
|
|
||
|
def canonical?
|
||
|
type == TYPE_CANONICAL
|
||
|
end
|
||
|
|
||
|
# TODO: remove. Kept for backwards compatibility with ::Permission model
|
||
|
def table?
|
||
|
type == TYPE_CANONICAL
|
||
|
end
|
||
|
|
||
|
def map?
|
||
|
kuviz? || derived?
|
||
|
end
|
||
|
|
||
|
def derived?
|
||
|
type == TYPE_DERIVED
|
||
|
end
|
||
|
|
||
|
def remote?
|
||
|
type == TYPE_REMOTE
|
||
|
end
|
||
|
|
||
|
def kuviz?
|
||
|
type == TYPE_KUVIZ
|
||
|
end
|
||
|
|
||
|
def app?
|
||
|
type == TYPE_APP
|
||
|
end
|
||
|
|
||
|
def layers
|
||
|
map ? map.layers : []
|
||
|
end
|
||
|
|
||
|
def data_layers
|
||
|
map ? map.data_layers : []
|
||
|
end
|
||
|
|
||
|
def carto_layers
|
||
|
map ? map.carto_layers : []
|
||
|
end
|
||
|
|
||
|
def user_layers
|
||
|
map ? map.user_layers : []
|
||
|
end
|
||
|
|
||
|
def torque_layers
|
||
|
map ? map.torque_layers : []
|
||
|
end
|
||
|
|
||
|
def other_layers
|
||
|
map ? map.other_layers : []
|
||
|
end
|
||
|
|
||
|
def base_layers
|
||
|
map ? map.base_layers : []
|
||
|
end
|
||
|
|
||
|
def named_map_layers
|
||
|
map ? map.named_map_layers : []
|
||
|
end
|
||
|
|
||
|
def password_valid?(password)
|
||
|
password_protected? &&
|
||
|
Carto::Common::EncryptionService.verify(password: password, secure_password: encrypted_password,
|
||
|
salt: password_salt, secret: Cartodb.config[:password_secret])
|
||
|
end
|
||
|
|
||
|
def organization?
|
||
|
privacy == PRIVACY_PRIVATE && !permission.acl.empty?
|
||
|
end
|
||
|
|
||
|
def password_protected?
|
||
|
privacy == PRIVACY_PROTECTED
|
||
|
end
|
||
|
|
||
|
def private?
|
||
|
is_privacy_private? && !organization?
|
||
|
end
|
||
|
|
||
|
def is_privacy_private?
|
||
|
privacy == PRIVACY_PRIVATE
|
||
|
end
|
||
|
|
||
|
def public?
|
||
|
privacy == PRIVACY_PUBLIC
|
||
|
end
|
||
|
|
||
|
def public_with_link?
|
||
|
self.privacy == PRIVACY_LINK
|
||
|
end
|
||
|
|
||
|
def editable?
|
||
|
!(kind_raster? || type_slide?)
|
||
|
end
|
||
|
|
||
|
def get_auth_tokens
|
||
|
[get_auth_token]
|
||
|
end
|
||
|
|
||
|
def mapviews
|
||
|
@mapviews ||= CartoDB::Visualization::Stats.mapviews(stats)
|
||
|
end
|
||
|
|
||
|
def total_mapviews
|
||
|
@total_mapviews ||= CartoDB::Visualization::Stats.new(self, nil).total_mapviews
|
||
|
end
|
||
|
|
||
|
def geometry_types
|
||
|
@geometry_types ||= user_table.geometry_types if user_table
|
||
|
end
|
||
|
|
||
|
def table_service
|
||
|
user_table.try(:service)
|
||
|
end
|
||
|
|
||
|
def has_read_permission?(user)
|
||
|
user && (owner?(user) || (permission && permission.user_has_read_permission?(user)))
|
||
|
end
|
||
|
alias :can_view_private_info? :has_read_permission?
|
||
|
|
||
|
def estimated_row_count
|
||
|
user_table.try(:row_count)
|
||
|
end
|
||
|
|
||
|
def actual_row_count
|
||
|
user_table.try(:actual_row_count)
|
||
|
end
|
||
|
|
||
|
def license_info
|
||
|
if !license.nil?
|
||
|
Carto::License.find(license.to_sym)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def can_be_cached?
|
||
|
!is_privacy_private?
|
||
|
end
|
||
|
|
||
|
def widgets
|
||
|
# Preload widgets for all layers
|
||
|
ActiveRecord::Associations::Preloader.new.preload(layers, :widgets)
|
||
|
layers.map(&:widgets).flatten
|
||
|
end
|
||
|
|
||
|
def analysis_widgets
|
||
|
widgets.select { |w| w.source_id.present? }
|
||
|
end
|
||
|
|
||
|
def attributions_from_derived_visualizations
|
||
|
related_canonical_visualizations.map(&:attributions).reject(&:blank?)
|
||
|
end
|
||
|
|
||
|
def delete_from_table
|
||
|
destroy if persisted?
|
||
|
end
|
||
|
|
||
|
def allowed_auth_tokens
|
||
|
entities = [user] + permission.entities_with_read_permission
|
||
|
entities.map(&:get_auth_token)
|
||
|
end
|
||
|
|
||
|
# - v2 (Editor): not private
|
||
|
# - v3 (Builder): not derived or not private, mapcapped
|
||
|
# This Ruby code should match the SQL code at Carto::VisualizationQueryBuilder#build section for @only_published.
|
||
|
def published?
|
||
|
!is_privacy_private? && (!builder? || !derived? || mapcapped?)
|
||
|
end
|
||
|
|
||
|
def builder?
|
||
|
version == VERSION_BUILDER
|
||
|
end
|
||
|
|
||
|
MAX_MAPCAPS_PER_VISUALIZATION = 1
|
||
|
|
||
|
def create_mapcap!
|
||
|
unless mapcaps.count < MAX_MAPCAPS_PER_VISUALIZATION
|
||
|
mapcaps.last.destroy
|
||
|
end
|
||
|
|
||
|
auto_generate_indices_for_all_layers
|
||
|
mapcaps.create!
|
||
|
end
|
||
|
|
||
|
def mapcapped?
|
||
|
latest_mapcap.present?
|
||
|
end
|
||
|
|
||
|
def latest_mapcap
|
||
|
mapcaps.first
|
||
|
end
|
||
|
|
||
|
def uses_builder_features?
|
||
|
builder? || analyses.any? || widgets.any? || mapcapped?
|
||
|
end
|
||
|
|
||
|
def add_source_analyses
|
||
|
return unless analyses.empty?
|
||
|
|
||
|
data_layers.each_with_index do |layer, index|
|
||
|
analysis = Carto::Analysis.source_analysis_for_layer(layer, index)
|
||
|
|
||
|
if analysis.save
|
||
|
layer.options[:source] = analysis.natural_id
|
||
|
layer.options[:letter] = analysis.natural_id.first
|
||
|
layer.save
|
||
|
else
|
||
|
log_warning(message: "Couldn't add source analysis for layer", current_user: user, layer: layer.attributes)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def ids_json
|
||
|
layers_for_hash = layers.map do |layer|
|
||
|
{ layer_id: layer.id, widgets: layer.widgets.map(&:id) }
|
||
|
end
|
||
|
|
||
|
{
|
||
|
visualization_id: id,
|
||
|
map_id: map.id,
|
||
|
layers: layers_for_hash
|
||
|
}
|
||
|
end
|
||
|
|
||
|
def populate_ids(ids_json)
|
||
|
self.id = ids_json[:visualization_id]
|
||
|
map.id = ids_json[:map_id]
|
||
|
|
||
|
map.layers.each_with_index do |layer, index|
|
||
|
stored_layer_ids = ids_json[:layers][index]
|
||
|
stored_layer_id = stored_layer_ids[:layer_id]
|
||
|
|
||
|
layer.id = stored_layer_id
|
||
|
layer.maps = [map]
|
||
|
|
||
|
layer.widgets.each_with_index do |widget, widget_index|
|
||
|
widget.id = stored_layer_ids[:widgets][widget_index]
|
||
|
widget.layer_id = stored_layer_id
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def for_presentation
|
||
|
mapcapped? ? latest_mapcap.regenerate_visualization : self
|
||
|
end
|
||
|
|
||
|
# TODO: we should make visualization privacy/security methods aware of mapcaps and make those
|
||
|
# deal with all the different the cases internally.
|
||
|
# See https://github.com/CartoDB/cartodb/pull/9678
|
||
|
def non_mapcapped
|
||
|
persisted? ? self : Carto::Visualization.find(id)
|
||
|
end
|
||
|
|
||
|
def mark_as_vizjson2
|
||
|
$tables_metadata.SADD(V2_VISUALIZATIONS_REDIS_KEY, id)
|
||
|
end
|
||
|
|
||
|
def uses_vizjson2?
|
||
|
$tables_metadata.SISMEMBER(V2_VISUALIZATIONS_REDIS_KEY, id) > 0
|
||
|
end
|
||
|
|
||
|
def open_in_editor?
|
||
|
!builder? && uses_vizjson2?
|
||
|
end
|
||
|
|
||
|
def can_be_automatically_migrated_to_v3?
|
||
|
overlays.builder_incompatible.none?
|
||
|
end
|
||
|
|
||
|
def state
|
||
|
super ? super : build_state
|
||
|
end
|
||
|
|
||
|
def can_be_private?
|
||
|
derived? ? user.try(:private_maps_enabled) : user.try(:private_tables_enabled)
|
||
|
end
|
||
|
|
||
|
# TODO: Backward compatibility with Sequel
|
||
|
def store_using_table(_table_privacy_changed = false)
|
||
|
store
|
||
|
end
|
||
|
|
||
|
def store
|
||
|
save!
|
||
|
self
|
||
|
end
|
||
|
|
||
|
def is_owner?(user)
|
||
|
user && user.id == user_id
|
||
|
end
|
||
|
|
||
|
def unlink_from(user_table)
|
||
|
layers_dependent_on(user_table).each do |layer|
|
||
|
Carto::Analysis.find_by_natural_id(id, layer.source_id).try(:destroy) if layer.source_id
|
||
|
|
||
|
layer.destroy
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def invalidate_after_commit
|
||
|
# This marks this visualization as affected by this transaction, so AR will call its `after_commit` hook, which
|
||
|
# performs the actual invalidations. This takes this operation outside of the DB transaction to avoid long locks
|
||
|
if self.class.connection.open_transactions.zero?
|
||
|
raise 'invalidate_after_commit should be called within a transaction'
|
||
|
end
|
||
|
add_to_transaction
|
||
|
true
|
||
|
end
|
||
|
# TODO: Privacy manager compatibility, can be removed after removing ::UserTable
|
||
|
alias :invalidate_cache :invalidate_after_commit
|
||
|
|
||
|
def has_permission?(user, permission_type)
|
||
|
return false if user.viewer && permission_type == Carto::Permission::ACCESS_READWRITE
|
||
|
return is_owner?(user) if permission_id.nil?
|
||
|
is_owner?(user) || permission.permitted?(user, permission_type)
|
||
|
end
|
||
|
|
||
|
def ensure_valid_privacy
|
||
|
self.privacy = default_privacy if privacy.nil?
|
||
|
self.privacy = PRIVACY_PUBLIC unless can_be_private?
|
||
|
end
|
||
|
|
||
|
def default_privacy
|
||
|
can_be_private? ? PRIVACY_LINK : PRIVACY_PUBLIC
|
||
|
end
|
||
|
|
||
|
def privacy=(privacy)
|
||
|
super(privacy.try(:downcase))
|
||
|
end
|
||
|
|
||
|
def password=(value)
|
||
|
if value.present?
|
||
|
self.password_salt = ""
|
||
|
self.encrypted_password = Carto::Common::EncryptionService.encrypt(password: value,
|
||
|
secret: Cartodb.config[:password_secret])
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def synced?
|
||
|
synchronization.present?
|
||
|
end
|
||
|
|
||
|
def dependent_visualizations
|
||
|
user_table&.dependent_visualizations || []
|
||
|
end
|
||
|
|
||
|
def faster_dependent_visualizations(limit: nil)
|
||
|
@faster_dependent_visualizations ||= user_table&.faster_dependent_visualizations(limit: limit)
|
||
|
end
|
||
|
|
||
|
def dependent_visualizations_count
|
||
|
user_table&.dependent_visualizations_count.to_i
|
||
|
end
|
||
|
|
||
|
def backup_visualization(category = Carto::VisualizationBackup::CATEGORY_VISUALIZATION)
|
||
|
return true if remote?
|
||
|
|
||
|
if map && !destroyed?
|
||
|
create_visualization_backup(visualization: self, category: category)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def subscription
|
||
|
if user_table
|
||
|
doss = Carto::DoSyncServiceFactory.get_for_user(user)
|
||
|
doss&.subscription_from_sync_table(user_table.name)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def remove_password
|
||
|
self.password_salt = nil
|
||
|
self.encrypted_password = nil
|
||
|
end
|
||
|
|
||
|
def perform_invalidations
|
||
|
invalidation_service.invalidate
|
||
|
rescue StandardError => e
|
||
|
# This is called at an after_commit. If there's any error, we won't notice
|
||
|
# but the after_commit chain stops.
|
||
|
# This was discovered during #12844, because "Updates changes even if named maps communication fails" test
|
||
|
# begun failing because Overlay#invalidate_cache invokes this method directly.
|
||
|
# We chose to log and continue to keep coherence on calls to this outside the callback.
|
||
|
log_error(message: "Error on visualization invalidation", exception: e, visualization: { id: id })
|
||
|
end
|
||
|
|
||
|
def propagate_privacy_and_name_to
|
||
|
raise "Empty table sent to propagate_privacy_and_name_to()" unless table
|
||
|
propagate_privacy if privacy_changed? && canonical?
|
||
|
propagate_name if name_was != name # name_changed? returns false positives in changes like a->A->a (sanitization)
|
||
|
end
|
||
|
|
||
|
def propagate_privacy
|
||
|
table.reload
|
||
|
if privacy && table.privacy_text.casecmp(privacy) != 0 # privacy is different, case insensitive
|
||
|
CartoDB::TablePrivacyManager.new(table).set_from_visualization(self).update_cdb_tablemetadata
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def propagate_name
|
||
|
# TODO: Move this to ::Table?
|
||
|
return if table.changing_name?
|
||
|
table.register_table_only = register_table_only
|
||
|
table.name = name
|
||
|
if table.name != name
|
||
|
# Sanitization. For example, spaces -> _
|
||
|
update_column(:name, table.name)
|
||
|
end
|
||
|
table.update(name: name)
|
||
|
|
||
|
if name_changed?
|
||
|
support_tables.rename(name_was, name, true, name_was)
|
||
|
end
|
||
|
self
|
||
|
rescue StandardError => exception
|
||
|
if name_changed? && !(exception.to_s =~ /relation.*does not exist/)
|
||
|
revert_name_change(name_was)
|
||
|
end
|
||
|
raise CartoDB::InvalidMember.new(exception.to_s)
|
||
|
end
|
||
|
|
||
|
def revert_name_change(previous_name)
|
||
|
self.name = previous_name
|
||
|
store
|
||
|
rescue StandardError => exception
|
||
|
raise CartoDB::InvalidMember.new(exception.to_s)
|
||
|
end
|
||
|
|
||
|
def propagate_attribution_change
|
||
|
table.propagate_attribution_change(attributions) if table && attributions_changed?
|
||
|
end
|
||
|
|
||
|
def support_tables
|
||
|
@support_tables ||= CartoDB::Visualization::SupportTables.new(
|
||
|
user.in_database, parent_id: id, parent_kind: kind, public_user_roles: user.db_service.public_user_roles
|
||
|
)
|
||
|
end
|
||
|
|
||
|
def auto_generate_indices_for_all_layers
|
||
|
user_tables = data_layers.map(&:user_tables).flatten.uniq
|
||
|
user_tables.each do |ut|
|
||
|
::Resque.enqueue(::Resque::UserDBJobs::UserDBMaintenance::AutoIndexTable, ut.id)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def set_random_id
|
||
|
# This should be done with a DB default
|
||
|
self.id ||= random_uuid
|
||
|
end
|
||
|
|
||
|
def set_default_permission
|
||
|
self.permission ||= Carto::Permission.create(owner: user, owner_username: user.username)
|
||
|
end
|
||
|
|
||
|
def has_private_tables?
|
||
|
!related_tables.index { |table| table.private? }.nil?
|
||
|
end
|
||
|
|
||
|
def sorted_related_table_names
|
||
|
mapped_table_names = related_tables.map { |table| "#{user.database_schema}.#{table.name}" }
|
||
|
|
||
|
mapped_table_names.sort { |i, j| i <=> j }.join(',')
|
||
|
end
|
||
|
|
||
|
def get_related_tables
|
||
|
return [] unless map
|
||
|
|
||
|
map.data_layers.flat_map(&:user_tables).uniq
|
||
|
end
|
||
|
|
||
|
def get_related_canonical_visualizations
|
||
|
get_related_visualizations_by_types([TYPE_CANONICAL])
|
||
|
end
|
||
|
|
||
|
def get_related_visualizations_by_types(types)
|
||
|
Carto::Visualization.where(map_id: related_tables.map(&:map_id), type: types).all
|
||
|
end
|
||
|
|
||
|
def has_write_permission?(user)
|
||
|
user && !user.viewer? && (owner?(user) || (permission && permission.user_has_write_permission?(user)))
|
||
|
end
|
||
|
|
||
|
def owner?(user)
|
||
|
user_id == user.id
|
||
|
end
|
||
|
|
||
|
def validate_password_presence
|
||
|
errors.add(:password, 'required for protected visualization') if password_protected? && !has_password?
|
||
|
end
|
||
|
|
||
|
def remove_password_if_unprotected
|
||
|
remove_password unless password_protected?
|
||
|
end
|
||
|
|
||
|
def validate_privacy_changes
|
||
|
return unless privacy_changed? && (map? || table?)
|
||
|
|
||
|
is_privacy_private? ? validate_change_to_private : validate_change_to_public
|
||
|
end
|
||
|
|
||
|
def validate_change_to_private
|
||
|
if (!user&.private_tables_enabled? && table?) || (!user&.private_maps_enabled? && map?)
|
||
|
errors.add(:privacy, 'cannot be set to private')
|
||
|
end
|
||
|
|
||
|
return unless !privacy_was || privacy_was != Carto::Visualization::PRIVACY_PRIVATE
|
||
|
|
||
|
if map? && CartoDB::QuotaChecker.new(user).will_be_over_private_map_quota?
|
||
|
errors.add(:privacy, 'over account private map quota')
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def validate_change_to_public
|
||
|
return unless !privacy_was || privacy_was == Carto::Visualization::PRIVACY_PRIVATE
|
||
|
|
||
|
if map? && CartoDB::QuotaChecker.new(user).will_be_over_public_map_quota?
|
||
|
errors.add(:privacy, 'over account public map quota')
|
||
|
end
|
||
|
|
||
|
if table? && CartoDB::QuotaChecker.new(user).will_be_over_public_dataset_quota?
|
||
|
errors.add(:privacy, 'over account public dataset quota')
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def validate_user_not_viewer
|
||
|
if user.viewer
|
||
|
errors.add(:user, 'cannot be viewer')
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def invalidation_service
|
||
|
@invalidation_service ||= Carto::VisualizationInvalidationService.new(self)
|
||
|
end
|
||
|
|
||
|
class Watcher
|
||
|
# watcher:_orgid_:_vis_id_:_user_id_
|
||
|
KEY_FORMAT = "watcher:%s".freeze
|
||
|
|
||
|
# @params user Carto::User
|
||
|
# @params visualization Carto::Visualization
|
||
|
# @throws Carto::Visualization::WatcherError
|
||
|
def initialize(user, visualization, notification_ttl = nil)
|
||
|
raise WatcherError.new('User must belong to an organization') if user.organization.nil?
|
||
|
@user = user
|
||
|
@visualization = visualization
|
||
|
|
||
|
default_ttl = Cartodb.get_config(:watcher, 'ttl') || 60
|
||
|
@notification_ttl = notification_ttl.nil? ? default_ttl : notification_ttl
|
||
|
end
|
||
|
|
||
|
# Notifies that is editing the visualization
|
||
|
# NOTE: Expiration is handled internally by redis
|
||
|
def notify
|
||
|
key = KEY_FORMAT % @visualization.id
|
||
|
$tables_metadata.multi do
|
||
|
$tables_metadata.hset(key, @user.username, current_timestamp + @notification_ttl)
|
||
|
$tables_metadata.expire(key, @notification_ttl)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Returns a list of usernames currently editing the visualization
|
||
|
def list
|
||
|
key = KEY_FORMAT % @visualization.id
|
||
|
users_expiry = $tables_metadata.hgetall(key)
|
||
|
now = current_timestamp
|
||
|
users_expiry.select { |_, expiry| expiry.to_i > now }.keys
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def current_timestamp
|
||
|
Time.now.getutc.to_i
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class WatcherError < CartoDB::BaseCartoDBError; end
|
||
|
class AlreadyLikedError < StandardError; end
|
||
|
class UnauthorizedLikeError < StandardError; end
|
||
|
end
|