Initial commit
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user