257 lines
7.4 KiB
Ruby
257 lines
7.4 KiB
Ruby
|
require_relative 'layer/presenter'
|
||
|
require_relative 'table/user_table'
|
||
|
require_relative '../../lib/cartodb/stats/editor_apis'
|
||
|
require_dependency 'carto/table_utils'
|
||
|
require_dependency 'carto/query_rewriter'
|
||
|
require_relative 'carto/layer'
|
||
|
|
||
|
class Layer < Sequel::Model
|
||
|
include Carto::TableUtils
|
||
|
include Carto::LayerTableDependencies
|
||
|
include Carto::QueryRewriter
|
||
|
|
||
|
plugin :serialization, :json, :options, :infowindow, :tooltip
|
||
|
|
||
|
ALLOWED_KINDS = %W{ carto tiled background gmapsbase torque wms }
|
||
|
BASE_LAYER_KINDS = %w(tiled background gmapsbase wms)
|
||
|
DATA_LAYER_KINDS = ALLOWED_KINDS - BASE_LAYER_KINDS
|
||
|
|
||
|
PUBLIC_ATTRIBUTES = %W{ options kind infowindow tooltip id order }
|
||
|
|
||
|
TEMPLATES_MAP = {
|
||
|
'table/views/infowindow_light' => 'infowindow_light',
|
||
|
'table/views/infowindow_dark' => 'infowindow_dark',
|
||
|
'table/views/infowindow_light_header_blue' => 'infowindow_light_header_blue',
|
||
|
'table/views/infowindow_light_header_yellow' => 'infowindow_light_header_yellow',
|
||
|
'table/views/infowindow_light_header_orange' => 'infowindow_light_header_orange',
|
||
|
'table/views/infowindow_light_header_green' => 'infowindow_light_header_green',
|
||
|
'table/views/infowindow_header_with_image' => 'infowindow_header_with_image'
|
||
|
}
|
||
|
|
||
|
# Sets default order to the maximum order of the sibling layers + 1
|
||
|
def set_default_order(parent)
|
||
|
max_order = parent.layers_dataset.select(:order).map(&:order).compact.max
|
||
|
order = (max_order == nil ? 0 : max_order + 1)
|
||
|
self.update(:order => order) if self.order.blank?
|
||
|
end
|
||
|
|
||
|
one_to_many :layer_node_styles
|
||
|
many_to_many :maps, after_add: proc { |layer, parent| layer.after_added_to_map(parent) }
|
||
|
many_to_many :users, after_add: proc { |layer, parent| layer.set_default_order(parent) }
|
||
|
many_to_many :user_tables,
|
||
|
join_table: :layers_user_tables,
|
||
|
left_key: :layer_id, right_key: :user_table_id,
|
||
|
reciprocal: :layers, class: ::UserTable
|
||
|
|
||
|
plugin :association_dependencies, :maps => :nullify, :users => :nullify, :user_tables => :nullify
|
||
|
|
||
|
def public_values
|
||
|
Hash[ PUBLIC_ATTRIBUTES.map { |attribute| [attribute, send(attribute)] } ]
|
||
|
end
|
||
|
|
||
|
def validate
|
||
|
super
|
||
|
errors.add(:kind, "not accepted") unless ALLOWED_KINDS.include?(kind)
|
||
|
errors.add(:maps, "Viewer users can't edit layers") if maps.find { |m| m.user && m.user.viewer }
|
||
|
|
||
|
if ((Cartodb.config[:enforce_non_empty_layer_css] rescue true))
|
||
|
style = options.include?('tile_style') ? options['tile_style'] : nil
|
||
|
if style.nil? || style.strip.empty?
|
||
|
errors.add(:options, 'Tile style is empty')
|
||
|
stats_aggregator = CartoDB::Stats::EditorAPIs.instance
|
||
|
stats_aggregator.increment("errors.layer.empty-css")
|
||
|
stats_aggregator.increment("errors.total")
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def before_save
|
||
|
super
|
||
|
self.updated_at = Time.now
|
||
|
end
|
||
|
|
||
|
def to_json(*args)
|
||
|
public_values.merge(
|
||
|
infowindow: self.values[:infowindow].nil? ? {} : JSON.parse(self.values[:infowindow]),
|
||
|
tooltip: JSON.parse(self.values[:tooltip]),
|
||
|
options: self.values[:options].nil? ? {} : JSON.parse(self.values[:options])
|
||
|
).to_json(*args)
|
||
|
end
|
||
|
|
||
|
def after_save
|
||
|
super
|
||
|
maps.each(&:notify_map_change)
|
||
|
|
||
|
if data_layer?
|
||
|
register_table_dependencies
|
||
|
update_layer_node_style
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def before_destroy
|
||
|
raise CartoDB::InvalidMember.new(user: "Viewer users can't destroy layers") if user && user.viewer
|
||
|
maps.each(&:notify_map_change)
|
||
|
super
|
||
|
end
|
||
|
|
||
|
def key
|
||
|
"rails:layer_styles:#{self.id}"
|
||
|
end
|
||
|
|
||
|
def infowindow_template_path
|
||
|
if self.infowindow.present? && self.infowindow['template_name'].present?
|
||
|
template_name = TEMPLATES_MAP.fetch(self.infowindow['template_name'], self.infowindow['template_name'])
|
||
|
Rails.root.join("lib/assets/javascripts/cartodb/table/views/infowindow/templates/#{template_name}.jst.mustache")
|
||
|
else
|
||
|
nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def tooltip_template_path
|
||
|
if self.tooltip.present? && self.tooltip['template_name'].present?
|
||
|
template_name = TEMPLATES_MAP.fetch(self.tooltip['template_name'], self.tooltip['template_name'])
|
||
|
Rails.root.join("lib/assets/javascripts/cartodb/table/views/tooltip/templates/#{template_name}.jst.mustache")
|
||
|
else
|
||
|
nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def copy(override_attributes={})
|
||
|
attributes = public_values.select { |k, v| k != 'id' }.merge(override_attributes)
|
||
|
::Layer.new(attributes)
|
||
|
end
|
||
|
alias dup copy
|
||
|
|
||
|
def data_layer?
|
||
|
!base_layer?
|
||
|
end
|
||
|
|
||
|
def torque_layer?
|
||
|
kind == 'torque'
|
||
|
end
|
||
|
|
||
|
def base_layer?
|
||
|
BASE_LAYER_KINDS.include?(kind)
|
||
|
end
|
||
|
|
||
|
def basemap?
|
||
|
["gmapsbase", "tiled"].include?(kind)
|
||
|
end
|
||
|
|
||
|
def supports_labels_layer?
|
||
|
basemap? && options["labels"] && options["labels"]["urlTemplate"]
|
||
|
end
|
||
|
|
||
|
def register_table_dependencies(db=SequelRails.connection)
|
||
|
db.transaction do
|
||
|
delete_table_dependencies
|
||
|
insert_table_dependencies
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def rename_table(current_table_name, new_table_name)
|
||
|
return self unless data_layer? or torque_layer?
|
||
|
target_keys = %w{ table_name tile_style query }
|
||
|
|
||
|
options = JSON.parse(self[:options])
|
||
|
targets = options.select { |key, value| target_keys.include?(key) }
|
||
|
renamed = targets.map do |key, value|
|
||
|
[key, rename_in(value, current_table_name, new_table_name)]
|
||
|
end
|
||
|
|
||
|
self.options = options.merge(Hash[renamed])
|
||
|
self
|
||
|
end
|
||
|
|
||
|
def uses_private_tables?
|
||
|
user_tables.select(&:private?).any?
|
||
|
end
|
||
|
|
||
|
def legend
|
||
|
options['legend']
|
||
|
end
|
||
|
|
||
|
def get_presenter(options, configuration)
|
||
|
CartoDB::LayerModule::Presenter.new(self, options, configuration)
|
||
|
end
|
||
|
|
||
|
def set_option(key, value)
|
||
|
return unless data_layer?
|
||
|
|
||
|
self.options[key] = value
|
||
|
end
|
||
|
|
||
|
def qualified_table_name(viewer_user)
|
||
|
"#{viewer_user.sql_safe_database_schema}.#{safe_table_name_quoting(options['table_name'])}"
|
||
|
end
|
||
|
|
||
|
def user
|
||
|
map.user if map
|
||
|
end
|
||
|
|
||
|
def qualify_for_organization(owner_username)
|
||
|
options['query'] = qualify_query(query, options['table_name'], owner_username) if query
|
||
|
end
|
||
|
|
||
|
def after_added_to_map(map)
|
||
|
set_default_order(map)
|
||
|
register_table_dependencies
|
||
|
end
|
||
|
|
||
|
def source_id
|
||
|
options && options.symbolize_keys[:source]
|
||
|
end
|
||
|
|
||
|
def depends_on?(table)
|
||
|
user_tables.map(&:id).include?(table.id)
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def rename_in(target, anchor, substitution)
|
||
|
return if target.nil? || target.empty?
|
||
|
regex = /(\A|\W+)(#{anchor})(\W+|\z)/
|
||
|
target.gsub(regex) { |match| match.gsub(anchor, substitution) }
|
||
|
end
|
||
|
|
||
|
def delete_table_dependencies
|
||
|
# remove_* and remove_all_* do not delete the object from the database
|
||
|
# only disassociate the associated object from the receiver
|
||
|
user_tables.map { |table| remove_user_table(table) }
|
||
|
end
|
||
|
|
||
|
def insert_table_dependencies
|
||
|
affected_tables.map { |table| add_user_table(table) }
|
||
|
end
|
||
|
|
||
|
def tables_from_names(table_names, user)
|
||
|
::Table.get_all_by_names(table_names, user)
|
||
|
end
|
||
|
|
||
|
def affected_table_names(query)
|
||
|
query_tables = user.in_database["SELECT unnest(CDB_QueryTablesText(?))", query]
|
||
|
query_tables.map(:unnest)
|
||
|
end
|
||
|
|
||
|
def map
|
||
|
maps.first
|
||
|
end
|
||
|
|
||
|
def query
|
||
|
options.symbolize_keys[:query]
|
||
|
end
|
||
|
|
||
|
def update_layer_node_style
|
||
|
style = current_layer_node_style
|
||
|
if style
|
||
|
style.update_from_layer(self)
|
||
|
style.save
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def current_layer_node_style
|
||
|
return nil unless source_id
|
||
|
LayerNodeStyle.find(layer_id: id, source_id: source_id) || LayerNodeStyle.new(layer: self, source_id: source_id)
|
||
|
end
|
||
|
end
|