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
+487
View File
@@ -0,0 +1,487 @@
require 'set'
require_relative './member'
require_relative './overlays'
require_relative '../shared_entity'
require_relative '../../../services/data-repository/structures/collection'
module CartoDB
module Visualization
SIGNATURE = 'visualizations'
PARTIAL_MATCH_QUERY = %Q{
to_tsvector(
'english', coalesce(name, '') || ' '
|| coalesce(description, '')
) @@ plainto_tsquery('english', ?)
OR CONCAT(name, ' ', description) ILIKE ?
}
class << self
attr_accessor :repository
end
class Collection
# 'unauthenticated' overrides other filters
# 'user_id' filtered by default if present upon fetch()
# 'locked' is filtered but before the rest
# 'exclude_shared' and
# 'only_shared' are other filtes applied
# 'only_liked'
AVAILABLE_FIELD_FILTERS = %w{name type description map_id privacy id parent_id}
# Keys in this list are the only filters that should be kept for calculating totals (if present)
FILTERS_ALLOWED_AT_TOTALS = [ :type, :user_id, :unauthenticated ]
FILTER_SHARED_YES = 'yes'
FILTER_SHARED_NO = 'no'
FILTER_SHARED_ONLY = 'only'
ALLOWED_ORDERING_FIELDS = [:mapviews, :row_count, :size].freeze
# Same as services/data-repository/backend/sequel.rb
PAGE = 1
PER_PAGE = 300
ALL_RECORDS = 999999
def initialize(options={})
@total_entries = 0
@collection = DataRepository::Collection.new(
signature: SIGNATURE,
repository: options.fetch(:repository, Visualization.repository),
member_class: Member
)
@can_paginate = true
@lazy_order_by = nil
@unauthenticated_flag = false
@user_id = nil
@type = nil
end
DataRepository::Collection::INTERFACE.each do |method_name|
define_method(method_name) do |*arguments, &block|
result = collection.send(method_name, *arguments, &block)
return self if result.is_a?(DataRepository::Collection)
result
end
end
# NOTES:
# - if 'user_id' is present as filter, will fetch visualizations shared with the user,
# except if 'exclude_shared' filter is also present and true,
# - 'only_shared' forces to use different flow because if there are no shared there's nothing else to do
# - 'locked' filter has special behaviour
# - If 'only_liked' it will return all favorited visualizations, not only user's.
def fetch(filters={})
filters = filters.dup # Avoid changing state
@user_id = filters.fetch(:user_id, nil)
filters = restrict_filters_if_unauthenticated(filters)
dataset = compute_sharing_filter_dataset(filters)
dataset = compute_liked_filter_dataset(dataset, filters)
if dataset.nil?
@total_entries = 0
collection.storage = Set.new
else
dataset = apply_filters(dataset, filters)
@total_entries = dataset.count
collection.storage = Set.new(paginate_and_get_entries(dataset, filters))
end
self
end
def delete_if(&block)
collection.delete_if(&block)
end
# This method is not used for anything but called from the DataRepository::Collection interface above
def store
self
end
# Counts the total results, only taking into account general filters like type or privacy or sharing options
# so no name or map_id filtering.
def count_total(filters={})
total_user_entries = 0
cleaned_filters = filters.keep_if { |key, |
FILTERS_ALLOWED_AT_TOTALS.include?(key.to_sym)
}
cleaned_filters.merge!({ exclude_shared: true })
cleaned_filters = restrict_filters_if_unauthenticated(cleaned_filters)
dataset = compute_sharing_filter_dataset(cleaned_filters)
unless dataset.nil?
dataset = apply_filters(dataset, cleaned_filters)
total_user_entries = dataset.count
end
total_user_entries
end
def count_query(filters={})
dataset = compute_sharing_filter_dataset(filters)
if dataset.nil?
0
else
dataset = compute_liked_filter_dataset(dataset, filters)
dataset.nil? ? 0 : apply_filters(dataset, filters).count
end
end
def destroy
map(&:delete)
self
end
def to_poro
map { |member| member.to_hash(related: false, table_data: true) }
end
# Warning, this is a cached count, do not use if adding/removing collection items
# @throws KeyError
def total_shared_entries(type = nil)
total = 0
unless @unauthenticated_flag
if @user_id.nil?
raise KeyError.new("Can't retrieve shared count without specifying user id")
else
total = user_shared_entities_count(type) + organization_shared_entities_count(type)
end
end
total
end
attr_reader :total_entries
private
attr_reader :collection
def paginate_and_get_entries(dataset, filters)
if @can_paginate
dataset = repository.paginate(dataset, filters, @total_entries)
dataset.map { |attributes|
Visualization::Member.new(attributes)
}
else
items = dataset.map { |attributes|
Visualization::Member.new(attributes)
}
items = lazy_order_by(items, @lazy_order_by)
# Manual paging
page = (filters.delete(:page) || PAGE).to_i
per_page = (filters.delete(:per_page) || PER_PAGE).to_i
items.slice((page - 1) * per_page, per_page)
end
end
def user_shared_entities_count(type = nil)
type ||= @type
user_shared_count = CartoDB::SharedEntity.select(:entity_id)
.where(recipient_id: @user_id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER)
if type.nil?
user_shared_count = user_shared_count.join(:visualizations,
visualizations__id: :entity_id)
else
user_shared_count = user_shared_count.join(:visualizations,
visualizations__id: :entity_id,
type: type)
end
user_shared_count.count
end
def organization_shared_entities_count(type)
type ||= @type
user = ::User.where(id: @user_id).first
if user.nil? || user.organization.nil?
0
else
org_shared_count = CartoDB::SharedEntity.select(:entity_id)
.where(:recipient_id => user.organization_id,
:entity_type => CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION,
:recipient_type => CartoDB::SharedEntity::RECIPIENT_TYPE_ORGANIZATION)
if type.nil?
org_shared_count = org_shared_count.join(:visualizations,
visualizations__id: :entity_id)
else
org_shared_count = org_shared_count.join(:visualizations,
visualizations__id: :entity_id,
type: type)
end
org_shared_count.count
end
end
# If special filter unauthenticated: true is present, will restrict data
def restrict_filters_if_unauthenticated(filters)
@unauthenticated_flag = false
unless filters.delete(:unauthenticated).nil?
filters[:only_shared] = false
filters[:exclude_shared] = true
filters[:privacy] = Visualization::Member::PRIVACY_PUBLIC
filters.delete(:locked)
filters.delete(:map_id)
@unauthenticated_flag = true
end
filters
end
def base_collection(filters)
only_liked = filters.fetch(:only_liked, 'false')
if only_liked == true || only_liked == 'true'
user_id = filters[:user_id]
dataset = repository.collection({}, [])
dataset = add_liked_by_conditions_to_dataset(dataset, user_id)
else
repository.collection(filters, %w{ user_id })
end
end
def compute_sharing_filter_dataset(filters)
shared_filter = filters.delete(:shared)
case shared_filter
when FILTER_SHARED_YES
filters[:only_shared] = false
filters[:exclude_shared] = false
when FILTER_SHARED_NO
filters[:only_shared] = false
filters[:exclude_shared] = true
when FILTER_SHARED_ONLY
filters[:only_shared] = true
filters[:exclude_shared] = false
end
if filters[:only_shared].present? && filters[:only_shared].to_s == 'true'
dataset = repository.collection
dataset = filter_by_only_shared(dataset, filters)
else
dataset = base_collection(filters)
locked_filter = filters.delete(:locked)
unless locked_filter.nil?
if locked_filter.to_s == 'true'
locked_filter = true
filters[:exclude_shared] = true
else
locked_filter = locked_filter.to_s == 'false' ? false : nil
end
end
dataset = repository.apply_filters(dataset, {locked: locked_filter}, ['locked']) unless locked_filter.nil?
dataset = include_shared_entities(dataset, filters)
end
dataset
end
def compute_liked_filter_dataset(dataset, filters)
only_liked = filters.delete(:only_liked)
if [true, 'true'].include?(only_liked)
if @user_id.nil?
nil
else
filters[:order] = :updated_at if filters.fetch(:order, nil).nil?
liked_vis = user_liked_vis(@user_id)
if liked_vis.nil? || liked_vis.empty?
nil
else
dataset.where(id: liked_vis)
end
end
else
dataset
end
end
def add_liked_by_conditions_to_dataset(dataset, user_id)
user_shared_vis = user_shared_vis(user_id)
dataset.where {
Sequel.|(
{ privacy: [CartoDB::Visualization::Member::PRIVACY_PUBLIC, CartoDB::Visualization::Member::PRIVACY_LINK] },
{ user_id: user_id },
{ visualizations__id: user_shared_vis }
)
}
# TODO: this probably introduces duplicates. See #2899.
# Should be removed when like count and list matches for organizations
# include_shared_entities(dataset, { user_id: user_id } )
end
def apply_filters(dataset, filters)
@type = filters.fetch(:type, nil)
@type = nil if @type == ''
applied_filters = AVAILABLE_FIELD_FILTERS.dup
applied_filters = applied_filters.delete_if { |k, v| k == 'type' } if @type.nil?
dataset = repository.apply_filters(dataset, filters, applied_filters)
# TODO: symbolize types key
dataset = filter_by_types(dataset, filters.fetch('types', nil))
dataset = filter_by_tags(dataset, tags_from(filters))
dataset = filter_by_partial_match(dataset, filters.delete(:q))
dataset = filter_by_kind(dataset, filters.delete(:exclude_raster))
dataset = filter_by_min_date('updated_at', dataset, filters.delete(:min_updated_at)) if filters.has_key?(:min_updated_at)
dataset = filter_by_min_date('created_at', dataset, filters.delete(:min_created_at)) if filters.has_key?(:min_created_at)
dataset = filter_by_ids(dataset, filters.delete(:ids))
dataset = filter_by_permission_id(dataset, filters.delete(:permission_id))
dataset = filter_by_version(dataset, filters.delete(:version))
order_desc = filters.delete(:order_asc_desc)
order(dataset, filters.delete(:order), order_desc.nil? || order_desc == :desc)
end
# Note: Not implemented ascending order for now, all are descending sorts
def lazy_order_by(objects, field)
case field
when :mapviews
lazy_order_by_mapviews(objects)
when :row_count
lazy_order_by_row_count(objects)
when :size
lazy_order_by_size(objects)
end
end
def lazy_order_by_mapviews(objects)
# Stats have format [ date, value ]
viz_and_views = objects.map { |viz| [viz, viz.stats.map { |o| o[1] }.reduce(0, :+)] }
viz_and_views.sort! { |vv_a, vv_b| vv_b[1] <=> vv_a[1] }
viz_and_views.map { |vv| vv[0] }
end
def lazy_order_by_row_count(objects)
viz_and_rows = objects.map { |obj| [obj, (obj.table ? obj.table.row_count_and_size.fetch(:row_count, 0) : 0)] }
viz_and_rows.sort! { |vr_a, vr_b| vr_b[1] <=> vr_a[1] }
viz_and_rows.map { |vr| vr[0] }
end
def lazy_order_by_size(objects)
viz_and_size = objects.map { |obj| [obj, (obj.table ? obj.table.row_count_and_size.fetch(:size, 0) : 0)] }
viz_and_size.sort! { |vs_a, vs_b| vs_b[1] <=> vs_a[1] }
viz_and_size.map { |vs| vs[0] }
end
# Note: Not implemented ascending order for now
def order_by_related_attribute(dataset, criteria)
@can_paginate = false
@lazy_order_by = criteria
dataset
end
def order_by_base_attribute(dataset, criteria, order_desc = true)
@can_paginate = true
dataset.order(Sequel.send(order_desc.nil? || order_desc == true ? :desc : :asc, criteria))
end
# Allows to order by any CartoDB::Visualization::Member attribute (eg: updated_at, created_at), plus:
# - mapviews
# - row_count
# - size
# TODO: order_asc_desc only works for base attributes
def order(dataset, criteria=nil, order_desc = true)
return dataset if criteria.nil? || criteria.empty?
criteria = criteria.to_sym
if ALLOWED_ORDERING_FIELDS.include? criteria
order_by_related_attribute(dataset, criteria)
else
order_by_base_attribute(dataset, criteria, order_desc)
end
end
def filter_by_types(dataset, types = nil)
return dataset if types.nil? || types == ''
types_array = types.is_a?(String) ? types.split(',') : types
dataset.where(:type => types_array)
end
def filter_by_tags(dataset, tags=[])
return dataset if tags.nil? || tags.empty?
placeholders = tags.length.times.map { '?' }.join(', ')
filter = "tags && ARRAY[#{placeholders}]"
dataset.where([filter].concat(tags))
end
def filter_by_partial_match(dataset, pattern=nil)
return dataset if pattern.nil? || pattern.empty?
dataset.where(PARTIAL_MATCH_QUERY, pattern, "%#{pattern}%")
end
def filter_by_kind(dataset, filter_value)
return dataset if filter_value.nil? || !filter_value
dataset.where('kind=?', Member::KIND_GEOM)
end
def filter_by_min_date(column, dataset, date_filter)
return dataset if !date_filter
included = date_filter.has_key?(:include) ? date_filter[:include] : false
comparison = included ? '>=' : '>'
dataset.where("#{column} #{comparison} ?", date_filter[:date])
end
def filter_by_ids(dataset, ids)
return dataset if !ids
dataset.where(:id => ids)
end
def filter_by_permission_id(dataset, permission_id)
return dataset if permission_id.nil?
dataset.where(permission_id: permission_id)
end
def filter_by_version(dataset, version)
return dataset if version.nil?
dataset.where(version: version)
end
def filter_by_only_shared(dataset, filters)
return dataset \
unless (filters[:user_id].present? && filters[:only_shared].present? && filters[:only_shared].to_s == 'true')
shared_vis = user_shared_vis(filters[:user_id])
if shared_vis.nil? || shared_vis.empty?
nil
else
dataset.where(id: shared_vis).exclude(user_id: filters[:user_id])
end
end
def include_shared_entities(dataset, filters)
return dataset unless filters[:user_id].present?
return dataset if filters[:exclude_shared].present? && filters[:exclude_shared].to_s == 'true'
shared_vis = user_shared_vis(filters[:user_id])
return dataset if shared_vis.nil? || shared_vis.empty?
dataset.or(id: shared_vis)
end
def user_shared_vis(user_id)
recipient_ids = user_id.is_a?(Array) ? user_id : [user_id]
::User.where(id: user_id).each { |user|
if user.has_organization?
recipient_ids << user.organization.id
end
}
CartoDB::SharedEntity.where(
recipient_id: recipient_ids,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
).all
.map { |entity|
entity.entity_id
}
end
def user_liked_vis(user_id)
Like.where(actor: user_id).all.map{ |like| like.subject }
end
def tags_from(filters={})
filters.delete(:tags).to_s.split(',')
end
end
end
end
@@ -0,0 +1,54 @@
module CartoDB
module Visualization
class DerivedCreator
DEFAULT_MAP_NAME = 'Untitled Map'
def initialize(user, tables = [])
@rejected_layers = []
if tables.length > user.max_layers
tables.pop(tables.length - user.max_layers).each do |rejected_layers|
@rejected_layers << rejected_layers.name
end
end
@user = user
@tables = tables
end
def create
blender = CartoDB::Visualization::TableBlender.new(user, tables)
map = blender.blend
vis = Carto::Visualization.new(
name: beautify_name,
map_id: map.id,
type: Carto::Visualization::TYPE_DERIVED,
privacy: blender.blended_privacy,
user_id: user.id
)
unless user.private_maps_enabled
vis.privacy = Carto::Visualization::PRIVACY_PUBLIC
end
vis.save!
CartoDB::Visualization::Overlays.new(vis).create_default_overlays
[vis, @rejected_layers]
end
private
def beautify_name
if tables.length > 1
table = tables[0]
table.beautify_name(table.name)
else
DEFAULT_MAP_NAME
end
end
attr_reader :user, :tables, :rejected_layers
end
end
end
@@ -0,0 +1,33 @@
require 'sequel'
require_relative './member'
module CartoDB
module Visualization
class ExternalSource < Sequel::Model
many_to_one :visualization
def validate
validates_presence :visualization_id
validates_presence :import_url
# TODO: retrieve geometry_types
#validates_presence :geometry_types
validates_presence :rows_counted
validates_presence :size
end
def initialize(visualization_id, import_url, geometry_types, rows_counted, size, username = nil)
super({ visualization_id: visualization_id, import_url: import_url, geometry_types: geometry_types, rows_counted: rows_counted, size: size, username: username })
end
def importable_by?(user)
user.present? && visualization.user_id == user.id
end
def visualization
@visualization ||= CartoDB::Visualization::Member.new(id: visualization_id).fetch
end
end
end
end
+14
View File
@@ -0,0 +1,14 @@
module CartoDB
class Like < Sequel::Model
# PK is (actor,subject)
unrestrict_primary_key
# @param actor String (uuid)
# @param subject String (uuid)
# @param created_at DateTime
end
class AlreadyLikedError < StandardError; end
end
+83
View File
@@ -0,0 +1,83 @@
require_relative '../visualization'
require_relative './member'
require_relative '../user'
require_relative '../table'
require 'uuidtools'
module CartoDB
module Visualization
class Locator
def initialize(user_model=nil)
@user_model = user_model || ::User
end
def get(id_or_name, subdomain, filters={})
user = user_from(subdomain)
visualization_from(id_or_name, user, filters) ||
table_from(id_or_name, user) ||
[nil, nil]
end
private
attr_reader :user_model
def user_from(subdomain)
@user ||= user_model.where(username: subdomain).first
end
def visualization_from(id_or_name, user, filters)
visualization = nil
visualization = get_by_name(id_or_name, user, filters) if user
visualization = get_by_id(id_or_name, filters) if visualization.nil?
return false if visualization.nil?
[visualization, visualization.table]
end
def table_from(id_or_name, user)
table = ::Table.get_by_id(id_or_name, user)
return false unless table && table.table_visualization
[table.table_visualization, table]
rescue
false
end
def get_by_id(uuid, filters)
begin
::UUIDTools::UUID.parse(uuid)
rescue ArgumentError
return nil
end
params = {
id: uuid
}
Visualization::Collection.new.fetch(params.merge(filters)).first
rescue KeyError
nil
end
def get_by_name(name, user, filters)
params = {
name: name,
user_id: user.id
}
# when looking for a visualization using name return the ones that user owns
Visualization::Collection.new
.fetch(params.merge(filters))
.select { |u|
u.user_id == user.id
}
.first
rescue KeyError
nil
end
end
end
end
+878
View File
@@ -0,0 +1,878 @@
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
+23
View File
@@ -0,0 +1,23 @@
require_relative './collection'
module CartoDB
module Visualization
class NameChecker
def initialize(user)
@user = user
end
def available?(candidate)
!taken_names_for.include?(candidate)
end
private
def taken_names_for
@taken_names ||= Carto::Visualization::where(user_id: user.id).select(:name).map(&:name)
end
attr_reader :user
end # NameChecker
end # Visualization
end # CartoDB
@@ -0,0 +1,26 @@
require_relative './name_checker'
module CartoDB
module Visualization
class NameGenerator
PATTERN = 'Untitled map'
def initialize(user, checker=nil)
@user = user
@checker = checker || NameChecker.new(user)
end
def name(candidate=PATTERN, iteration=0)
candidate = (candidate || PATTERN).strip
new_candidate = iteration > 0 ? "#{candidate} #{iteration}" : candidate
return new_candidate if checker.available?(new_candidate)
name(candidate, iteration + 1)
end
private
attr_reader :checker, :user
end
end
end
+82
View File
@@ -0,0 +1,82 @@
module CartoDB
module Visualization
class Overlays
def initialize(visualization)
@visualization = visualization
end
def create_default_overlays
create_share_overlay(@visualization, 2)
create_search_overlay(@visualization, 3)
create_zoom_overlay(@visualization, 6)
create_loader_overlay(@visualization, 8)
# nil check added to support feature flag check (see #6108) without breaking backguards compatibility
if @visualization.user.nil? || !@visualization.user.has_feature_flag?('disabled_cartodb_logo')
create_logo_overlay(@visualization, 9)
end
end
private
def create_logo_overlay(member, order)
options = { display: true, x: 10, y: 40 }
Carto::Overlay.new(
order: order,
type: "logo",
template: '',
options: options,
visualization_id: member.id
).save
end
def generate_overlay(id, options, type, order)
Carto::Overlay.new(
order: order,
type: type,
template: "",
options: options,
visualization_id: id
)
end
def create_loader_overlay(member, order)
options = { display: true, x: 20, y: 150 }
Carto::Overlay.new(
order: order,
type: "loader",
template: '<div class="loader" original-title=""></div>',
options: options,
visualization_id: member.id
).save
end
def create_zoom_overlay(member, order)
options = { display: true, x: 20, y: 20 }
Carto::Overlay.new(
order: order,
type: "zoom",
template: '<a href="#zoom_in" class="zoom_in">+</a> <a href="#zoom_out" class="zoom_out">-</a>',
options: options,
visualization_id: member.id
).save
end
def create_share_overlay(member, order)
options = { display: true, x: 20, y: 20 }
generate_overlay(member.id, options, "share", order).save
end
def create_search_overlay(member, order)
options = { display: true, x: 60, y: 20 }
generate_overlay(member.id, options, "search", order).save
end
end
end
end
+171
View File
@@ -0,0 +1,171 @@
require_relative './member'
require_relative './external_source'
module CartoDB
module Visualization
class Presenter
def initialize(visualization, options={})
@visualization = visualization
@viewing_user = options.fetch(:user, nil)
@options = options
@table = options[:table] || visualization.table
@synchronization = options[:synchronization] || visualization.synchronization
# Expose real privacy (used for normal JSON purposes)
@real_privacy = options[:real_privacy] || false
end
def to_poro
permission = visualization.permission
poro = {
id: visualization.id,
name: visualization.name,
display_name: visualization.display_name,
map_id: visualization.map_id,
active_layer_id: visualization.active_layer_id,
type: visualization.type,
tags: visualization.tags,
description: visualization.description,
privacy: privacy_for_vizjson.upcase,
stats: visualization.stats,
created_at: visualization.created_at,
updated_at: visualization.updated_at,
permission: permission.nil? ? nil : CartoDB::PermissionPresenter.new(permission).to_poro,
locked: visualization.locked,
source: visualization.source,
title: visualization.title,
parent_id: visualization.parent_id,
license: visualization.license,
attributions: visualization.attributions,
kind: visualization.kind,
prev_id: visualization.prev_id,
next_id: visualization.next_id,
transition_options: visualization.transition_options,
active_child: visualization.active_child
}
poro.merge!(table: table_data_for(table, permission))
poro.merge!(external_source: external_source_data_for(visualization))
poro.merge!(synchronization: synchronization)
poro.merge!(related) if options.fetch(:related, true)
poro.merge!(children: children)
poro[:liked] = visualization.liked_by?(@viewing_user) unless @viewing_user.nil?
poro
end
def to_public_poro
{
id: visualization.id,
name: visualization.name,
display_name: visualization.display_name,
type: visualization.type,
tags: visualization.tags,
description: visualization.description,
updated_at: visualization.updated_at,
title: visualization.title,
kind: visualization.kind,
privacy: privacy_for_vizjson.upcase,
}
end
private
attr_reader :visualization, :options, :table, :synchronization
# Simplify certain privacy values for the vizjson
def privacy_for_vizjson
return @visualization.privacy if @real_privacy
case @visualization.privacy
when Member::PRIVACY_PUBLIC, Member::PRIVACY_LINK
Member::PRIVACY_PUBLIC
when Member::PRIVACY_PRIVATE
Member::PRIVACY_PRIVATE
when Member::PRIVACY_PROTECTED
Member::PRIVACY_PROTECTED
else
Member::PRIVACY_PRIVATE
end
end
def related
{ related_tables: related_tables }
end
def table_data_for(table=nil, permission = nil)
return {} unless table
table_name = table.name
unless @viewing_user.nil?
unless @visualization.is_owner?(@viewing_user)
table_name = "#{@visualization.user.sql_safe_database_schema}.#{table.name}"
end
end
table_data = {
id: table.id,
name: table_name,
permission: nil
}
table_visualization = table.table_visualization
unless table_visualization.nil?
presented_permission = if !permission.nil? && table_visualization.id == permission.entity_id
permission
else
table_visualization.permission
end
table_data[:permission] = CartoDB::PermissionPresenter.new(presented_permission).to_poro
table_data[:geometry_types] = table.geometry_types
end
table_data.merge!(
privacy: table.privacy_text_for_vizjson,
updated_at: table.updated_at
)
table_data.merge!(table.row_count_and_size)
table_data[:synchronization] = synchronization_data_for(table)
table_data
end
def external_source_data_for(visualization)
return {} unless visualization.type == Member::TYPE_REMOTE
external_source = Carto::ExternalSource.where(visualization_id: visualization.id).first
return {} unless external_source.present?
{
size: external_source.size,
row_count: external_source.rows_counted,
geometry_types: external_source.geometry_types
}
end
def children
@visualization.children.map { |vis| {
id: vis.id,
prev_id: vis.prev_id,
type: Visualization::Member::TYPE_SLIDE,
next_id: vis.next_id,
transition_options: vis.transition_options,
map_id: vis.map_id
}
}
end
def synchronization_data_for(table=nil)
return nil unless table
table.synchronization
end
def related_tables
without_associated_table(visualization.related_tables)
.map { |table| table_data_for(table) }
end
def without_associated_table(tables)
return tables unless visualization.table
tables.select { |table| table.id != visualization.table.id }
end
end
end
end
+168
View File
@@ -0,0 +1,168 @@
require_relative './stats'
require_relative '../visualization/collection'
require_relative './support_tables'
require_relative '../map'
require_relative '../layer'
module CartoDB
module Visualization
class Relator
LAYER_SCOPES = {
base: :user_layers,
cartodb: :carto_layers,
data: :data_layers,
others: :other_layers,
named_map: :named_maps_layers
}.freeze
INTERFACE = %w{ overlays user table related_templates related_tables related_canonical_visualizations
layers stats mapviews total_mapviews data_layers synchronization synced? permission
parent children support_tables prev_list_item next_list_item likes reload_likes
estimated_row_count actual_row_count }.freeze
def initialize(map, attributes = {})
@id = attributes.fetch(:id)
@user_id = attributes.fetch(:user_id)
@permission_id = attributes.fetch(:permission_id)
@parent_id = attributes.fetch(:parent_id)
@kind = attributes.fetch(:kind)
@support_tables = nil
@likes = nil
@prev_id = attributes.fetch(:prev_id)
@next_id = attributes.fetch(:next_id)
@map = map
end
# @return []
def children
ordered = []
children_vis = Visualization::Collection.new.fetch(parent_id: @id)
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
# @return CartoDB::Visualization::Member
def parent
@parent ||= Visualization::Member.new(id: @parent_id).fetch unless @parent_id.nil?
end
# @return CartoDB::Visualization::Member
def prev_list_item
@prev_vis ||= Visualization::Member.new(id: @prev_id).fetch unless @prev_id.nil?
end
# @return CartoDB::Visualization::Member
def next_list_item
@next_vis ||= Visualization::Member.new(id: @next_id).fetch unless @next_id.nil?
end
def support_tables
@support_tables ||= Visualization::SupportTables.new(
user.in_database, parent_id: @id, parent_kind: @kind, public_user_roles: user.db_service.public_user_roles)
end
def overlays
@overlays ||= Carto::Overlay.where(visualization_id: id).all
end
def user
@user ||= ::User[@user_id] unless @user_id.nil?
end
def table
return nil if map.nil?
@table ||= ::UserTable.from_map_id(map.id).try(:service)
end
def estimated_row_count
table.nil? ? nil : table.estimated_row_count
end
def actual_row_count
table.nil? ? nil : table.actual_row_count
end
def related_templates
Carto::Template.where(source_visualization_id: @id).all
end
def related_tables
@related_tables ||= layers(:data).flat_map { |layer| layer.user_tables.map(&:service) }.uniq(&:id)
end
def related_canonical_visualizations
@related_canonical_visualizations ||= get_related_canonical_visualizations
end
def layers(kind)
return [] unless map
map.send(LAYER_SCOPES.fetch(kind))
end
def synchronization
CartoDB::Synchronization::Member.new(visualization_id: @id).fetch_by_visualization_id
rescue KeyError
{}
end
def synced?
!synchronization.is_a?(Hash)
end
def stats(user=nil)
@stats ||= Visualization::Stats.new(self, user).to_poro
end
def mapviews(user=nil)
@mapviews ||= stats(user).collect { |o| o[1] }.reduce(:+)
end
def total_mapviews(user=nil)
@total_mapviews ||= Visualization::Stats.new(self, user).total_mapviews
end
def data_layers
layers(:data)
end
def permission
@permission ||= CartoDB::Permission.where(id: @permission_id).first unless @permission_id.nil?
end
def likes
@likes ||= likes_search.all.to_a
end
def reload_likes
@likes = nil
likes
end
attr_reader :id, :map
private
def likes_search
Like.where(subject: @id)
end
def get_related_canonical_visualizations
get_related_visualizations_by_types([CartoDB::Visualization::Member::TYPE_CANONICAL])
end
def get_related_visualizations_by_types(types)
related_map_ids = related_tables.map(&:map_id)
CartoDB::Visualization::Collection.new.fetch(map_id: related_map_ids, type: types)
end
end
end
end
+41
View File
@@ -0,0 +1,41 @@
require 'date'
module CartoDB
module Visualization
class Stats
def self.mapviews(stats)
stats.collect { |o| o[1] }.reduce(:+)
end
def initialize(visualization, user=nil)
@visualization = visualization
@user = user || visualization.user
end
def to_poro
new_calls = {}
CartoDB::Stats::APICalls.new.get_api_calls_with_dates(username, {stat_tag: visualization.id}).to_a.reverse.each do |call|
call_date = Date.parse(call[0]).strftime("%Y-%m-%d")
new_calls[call_date] = call[1]
end
return new_calls
end
def total_mapviews
CartoDB::Stats::APICalls.new.get_total_api_calls(username, visualization.id)
end
private
attr_reader :visualization, :user
def username
# TODO: remove this after adding visualizations --> users FK at #3508. Now it can crash.
user.nil? ? '' : user.username
end
end # Stats
end # Visualization
end # CartoDB
+149
View File
@@ -0,0 +1,149 @@
require_relative './member'
module CartoDB
module Visualization
class SupportTables
def initialize(database_connection, config={})
@database = database_connection
@parent_id = config.fetch(:parent_id, nil)
@parent_kind = config.fetch(:parent_kind, nil)
@public_user_roles_list = config.fetch(:public_user_roles)
@tables_list = nil
end
def reset
@tables_list = nil
end
# Only intended to be used if from the Visualization Relator (who will set the parent)
def load_actual_list(parent_name=nil)
return [] if @parent_id.nil? || @parent_kind != Visualization::Member::KIND_RASTER
parent = Visualization::Member.new(id:@parent_id).fetch
table_data = @database.fetch(%Q{
SELECT o_table_catalog AS catalog, o_table_schema AS schema, o_table_name AS name
FROM raster_overviews
WHERE r_table_catalog = '#{parent.user.database_name}'
AND r_table_schema = '#{parent.user.database_schema}'
AND r_table_name = '#{parent_name.nil? ? parent.name : parent_name}'
}).all
table_data.nil? ? [] : table_data
end
def delete_all
tables.each { |table|
@database.execute(%Q{
DROP TABLE "#{table[:schema]}"."#{table[:name]}"
})
}
end
# @param existing_parent_name String
# @param new_parent_name String
# @param recreate_relations Bool If true will recreate constraints and permissions from overviews
# @param seek_parent_name String|nil If specified, seeking of tables will be performed using this name
def rename(existing_parent_name, new_parent_name, recreate_relations=true, seek_parent_name=nil)
begin
schema = nil
support_tables_new_names = []
tables_list = tables(seek_parent_name)
tables_list.each { |item|
schema = item[:schema]
new_support_table_name = item[:name].dup
# CONVENTION: support_tables will always end in "_tablename", so we substitute using parent name
new_support_table_name.slice!(-existing_parent_name.length, existing_parent_name.length)
new_support_table_name = "#{new_support_table_name}#{new_parent_name}"
@database.execute(%Q{
ALTER TABLE "#{item[:schema]}"."#{item[:name]}" RENAME TO "#{new_support_table_name}"
})
support_tables_new_names.push(new_support_table_name)
}
renamed = true
rescue
renamed = false
end
if renamed && recreate_relations
support_tables_new_names.each { |table_name|
recreate_raster_constraints_if_exists(table_name, new_parent_name, schema)
update_permissions(table_name, @public_user_roles_list, schema)
}
end
{ success: renamed, names: support_tables_new_names }
end
def change_schema(new_schema, parent_table_name)
tables.each { |item|
@database.execute(%Q{
ALTER TABLE "#{item[:schema]}"."#{item[:name]}"
SET SCHEMA "#{new_schema}"
})
# Constraints are not automatically updated upon schema change or table renaming
recreate_raster_constraints_if_exists(item[:name], parent_table_name, new_schema)
update_permissions(item[:name], @public_user_roles_list, new_schema)
}
end
# For import purposes
# @param new_list Array [ { :schema, :name } ]
def tables=(new_list)
@tables_list = new_list
end
private
def tables(seek_parent_name=nil)
@tables_list ||= load_actual_list(seek_parent_name)
end
def update_permissions(overview_table_name, db_roles_list, schema)
overviews = @database.fetch(%Q{
SELECT o_table_name, o_table_schema
FROM raster_overviews
WHERE o_table_name = '#{overview_table_name}'
AND o_table_schema = '#{schema}'
}).first
return if overviews.nil?
@database.transaction do
db_roles_list.each { |role_name|
@database.execute(%Q{
GRANT SELECT ON TABLE "#{overviews[:o_table_schema]}"."#{overviews[:o_table_name]}" TO "#{role_name}"
})
}
end
end
# @see http://postgis.net/docs/manual-dev/using_raster_dataman.html#RT_Raster_Overviews
def recreate_raster_constraints_if_exists(overview_table_name, raster_table_name, schema)
constraint = @database.fetch(%Q{
SELECT o_table_name, o_raster_column, r_table_name, r_raster_column, overview_factor
FROM raster_overviews
WHERE o_table_name = '#{overview_table_name}'
AND o_table_schema = '#{schema}'
}).first
return if constraint.nil?
@database.transaction do
# @see http://postgis.net/docs/RT_DropOverviewConstraints.html
@database.execute(%Q{
SELECT DropOverviewConstraints('#{schema}', '#{constraint[:o_table_name]}',
'#{constraint[:o_raster_column]}')
})
# @see http://postgis.net/docs/manual-dev/RT_AddOverviewConstraints.html
@database.execute(%Q{
SELECT AddOverviewConstraints('#{schema}', '#{constraint[:o_table_name]}',
'#{constraint[:o_raster_column]}', '#{schema}', '#{raster_table_name}',
'#{constraint[:r_raster_column]}', #{constraint[:overview_factor]});
})
end
end
end
end
end
+47
View File
@@ -0,0 +1,47 @@
require_dependency 'map/copier'
module CartoDB
module Visualization
class TableBlender
def initialize(user, tables=[])
@user = user
@tables = tables
end
def blend
raise "Viewer users can't blend tables" if user.viewer
maps = tables.map(&:map)
copier = CartoDB::Map::Copier.new
destination_map = copier.new_map_from(maps.first)
destination_map.save
if @user.builder_enabled?
base_layer = Carto::LayerFactory.build_default_base_layer(@user)
destination_map.layers << base_layer
if base_layer.supports_labels_layer?
destination_map.layers << Carto::LayerFactory.build_default_labels_layer(base_layer)
end
else
copier.copy_base_layer(maps.first, destination_map)
end
maps.each { |map| copier.copy_data_layers(map, destination_map, user) }
destination_map.user_id = user.id
destination_map.save
destination_map
end
def blended_privacy
return Carto::Visualization::PRIVACY_PRIVATE if tables.any?(&:private?)
return Carto::Visualization::PRIVACY_LINK if tables.any?(&:public_with_link_only?)
Carto::Visualization::PRIVACY_PUBLIC
end
private
attr_reader :tables, :user
end
end
end
+148
View File
@@ -0,0 +1,148 @@
require_relative './member'
require_relative '../shared_entity'
module CartoDB
module Visualization
class Tags
DEFAULT_LIMIT = 500
def initialize(user, options={})
@user = user
@exclude_shared = options[:exclude_shared].present? && options[:exclude_shared] == true
end
def names(params={})
if only_shared?(params)
filter = shared_entities_sql_filter(params)
if filter.empty?
return []
else
Tag.fetch(%Q{
SELECT DISTINCT (unnest(tags)) as name
FROM visualizations
WHERE #{shared_entities_sql_filter(params)}
AND type IN ?
AND privacy IN ?
#{locked_from(params)}
LIMIT ?
}, types_from(params), privacy_from(params), limit_from(params)
).map{ |tag| tag.name}
end
else
Tag.fetch(%Q{
SELECT DISTINCT (unnest(tags)) as name
FROM visualizations
WHERE user_id = ?
AND type IN ?
AND privacy IN ?
#{locked_from(params)}
#{shared_entities_sql_filter(params)}
LIMIT ?
}, user.id, types_from(params), privacy_from(params), limit_from(params)
).map{ |tag| tag.name}
end
end
def count(params={})
if only_shared?(params)
filter = shared_entities_sql_filter(params)
if filter.empty?
return []
else
Tag.fetch(%Q{
WITH tags as (
SELECT unnest(tags) as name
FROM visualizations
WHERE #{shared_entities_sql_filter(params)}
AND type IN ?
#{locked_from(params)}
LIMIT ?
)
SELECT name, count(*) as count
FROM tags
GROUP BY name
ORDER BY count(*)
}, types_from(params), limit_from(params)
).all.map(&:values)
end
else
Tag.fetch(%Q{
WITH tags as (
SELECT unnest(tags) as name
FROM visualizations
WHERE user_id = ?
AND type IN ?
#{locked_from(params)}
#{shared_entities_sql_filter(params)}
LIMIT ?
)
SELECT name, count(*) as count
FROM tags
GROUP BY name
ORDER BY count(*)
}, user.id, types_from(params), limit_from(params)
).all.map(&:values)
end
end
private
attr_reader :user
def shared_entities_sql_filter(params)
return '' if @exclude_shared
only_shared = only_shared?(params)
ids = CartoDB::SharedEntity.where({
recipient_id: @user.id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
}).all.map { |entity|
entity.entity_id
}
return '' if ids.nil? || ids.empty?
if only_shared
"id IN ('#{ids.join("','")}')"
else
types_filter = types_from(params)
if types_filter.size == 1
types_fragment = " AND type IN ('#{types_filter.first}')"
else
types_fragment = ''
end
"OR (id IN ('#{ids.join("','")}') #{types_fragment})"
end
end
def locked_from(params={})
locked = params.fetch(:locked, nil)
if locked.nil?
""
else
locked = locked.to_s == 'true' ? 'true' : 'false'
"AND locked=#{locked}"
end
end
def only_shared?(params)
params[:only_shared].present? && params[:only_shared] == true
end
def privacy_from(params={})
privacy = params.fetch(:privacy, nil)
(privacy.nil? || privacy.empty?) ? Member::PRIVACY_VALUES : [privacy]
end
def types_from(params={})
type = params.fetch(:type, nil)
(type.nil? || type.empty?) ? [Member::TYPE_CANONICAL, Member::TYPE_DERIVED] : [type]
end
def limit_from(params={})
(params.fetch(:limit, DEFAULT_LIMIT) || DEFAULT_LIMIT).to_i
end
end
end
end
+232
View File
@@ -0,0 +1,232 @@
require 'json'
require 'ostruct'
require_relative '../layer/presenter'
require_relative '../layer_group/presenter'
require_relative '../named_map/presenter'
module CartoDB
module Visualization
class VizJSON
include Carto::HtmlSafe
VIZJSON_VERSION = '0.1.0'
def initialize(visualization, options = {}, configuration = {}, logger = nil)
@visualization = visualization
@map = visualization.map
@options = default_options.merge(options)
@configuration = configuration
@user = options.fetch(:user, nil)
logger.info(map.inspect) if logger
end
def to_export_poro(version = 1)
description = if visualization.description.blank?
""
else
clean_description(markdown_html_safe(visualization.description))
end
{
id: visualization.id,
version: VIZJSON_VERSION,
title: visualization.qualified_name(@user),
description: description,
scrollwheel: map.scrollwheel,
legends: map.legends,
url: options.delete(:url),
map_provider: map.provider,
bounds: bounds_from(map),
center: map.center,
zoom: map.zoom,
layers: all_layers_for(visualization),
overlays: overlays_for(visualization),
# Fields specific for this export
export_version: version,
# TODO: bug? @user is _viewer_user_, who might not be the owner
owner: { id: @user.id }
}
end
# Return a PORO (Hash object) for easy JSONification
# @see https://github.com/CartoDB/carto.js/blob/privacy-maps/doc/vizjson_format.md
def to_poro
poro_data = {
id: visualization.id,
version: VIZJSON_VERSION,
title: visualization.qualified_name(@user),
description: markdown_html_safe(visualization.description),
scrollwheel: map.scrollwheel,
legends: map.legends,
url: options.delete(:url),
map_provider: map.provider,
bounds: bounds_from(map),
center: map.center,
zoom: map.zoom,
updated_at: map.viz_updated_at,
layers: layers_for(visualization),
overlays: overlays_for(visualization),
prev: visualization.prev_id,
next: visualization.next_id,
transition_options: visualization.transition_options
}
auth_tokens = auth_tokens_for(visualization)
poro_data.merge!(auth_tokens: auth_tokens) if auth_tokens.length > 0
unless visualization.parent_id.nil?
poro_data[:title] = visualization.parent.qualified_name(@user)
poro_data[:description] = markdown_html_safe(visualization.parent.description)
end
poro_data
end
def layer_group_for(visualization)
LayerGroup::Presenter.new(visualization.layers(:cartodb), options, configuration).to_poro
end
def named_map_layer_group_for(visualization)
LayerGroup::Presenter.new(visualization.layers(:named_map), options, configuration).to_poro
end
def other_layers_for(visualization, named_maps_presenter = nil)
layer_index = visualization.layers(:cartodb).size
visualization.layers(:others).map do |layer|
if named_maps_presenter.nil?
decoration_data_to_apply = {}
else
decoration_data_to_apply = named_maps_presenter.get_decoration_for_layer(layer.kind, layer_index)
end
layer_index += 1
CartoDB::LayerModule::Presenter.new(layer, options, configuration, decoration_data_to_apply).to_vizjson_v2
end
end
private
attr_reader :visualization, :map, :options, :configuration
# Redcarpet markdown renderer adds "garbage" that would otherwise get reimported
def clean_description(description)
description.sub(/^<p>/, "").sub(/<\/p> ?(\n)?$/, "")
end
def bounds_from(map)
::JSON.parse("[#{map.view_bounds_sw}, #{map.view_bounds_ne}]")
rescue
# Do nothing
end
def all_layers_for(visualization)
layers_data = []
basemap_layer = basemap_layer_for(visualization)
layers_data.push(basemap_layer) if basemap_layer
data_layers = visualization.layers(:cartodb).map do |layer|
CartoDB::LayerModule::Presenter.new(layer, options, configuration).to_vizjson_v2
end
layers_data.push(data_layers)
layers_data.push(other_layers_for(visualization))
layers_data += non_basemap_base_layers_for(visualization)
layers_data.compact.flatten
end
def layers_for(visualization)
basemap_layer = basemap_layer_for(visualization)
layers_data = []
layers_data.push(basemap_layer) if basemap_layer
if visualization.retrieve_named_map?
presenter_options = {
user_name: options.fetch(:user_name),
api_key: options.delete(:user_api_key),
https_request: options.fetch(:https_request, false),
viewer_user: @user,
owner: visualization.user
}
named_maps_presenter = CartoDB::NamedMapsWrapper::Presenter.new(
visualization, layer_group_for_named_map(visualization), presenter_options, configuration
)
layers_data.push(named_maps_presenter.to_poro)
else
named_maps_presenter = nil
layers_data.push(layer_group_for(visualization))
end
layers_data.push(other_layers_for(visualization, named_maps_presenter))
layers_data += non_basemap_base_layers_for(visualization)
layers_data.compact.flatten
end
def layer_group_for_named_map(visualization)
layer_group_poro = layer_group_for(visualization)
# If there is *only* a torque layer, there is no layergroup
return {} if layer_group_poro.nil?
layers_data = Array.new
layer_num = 0
layer_group_poro[:options][:layer_definition][:layers].each do |layer|
layers_data.push(type: layer[:type],
options: layer[:options],
visible: layer[:visible],
index: layer_num
)
layer_num += 1
end
layers_data
end
# INFO: Assumes layers come always ordered by order (they do)
def basemap_layer_for(visualization)
layer = visualization.layers(:base).first
CartoDB::LayerModule::Presenter.new(layer, options, configuration).to_vizjson_v2 unless layer.nil?
end
# INFO: Assumes layers come always ordered by order (they do)
def non_basemap_base_layers_for(visualization)
base_layers = visualization.layers(:base)
if base_layers.length > 0
# Remove the basemap, which is always first
base_layers.slice(1, visualization.layers(:base).length)
.map do |layer|
CartoDB::LayerModule::Presenter.new(layer, options, configuration).to_vizjson_v2
end
else
[]
end
end
def overlays_for(visualization)
ordered_overlays_for(visualization).map do |overlay|
Carto::Api::OverlayPresenter.new(overlay).to_vizjson_poro
end
end
def ordered_overlays_for(visualization)
visualization.overlays.to_a
end
def default_options
{
full: true,
visualization_id: visualization.id,
https_request: false,
attributions: visualization.attributions_from_derived_visualizations,
for_named_map: false
}
end
def auth_tokens_for(visualization)
visualization.has_password? ? visualization.get_auth_tokens : []
end
end
end
end