397 lines
9.7 KiB
Ruby
397 lines
9.7 KiB
Ruby
|
require 'cartodb/per_request_sequel_cache'
|
||
|
require 'forwardable'
|
||
|
|
||
|
# This class is intended to deal exclusively with storage
|
||
|
class UserTable < Sequel::Model
|
||
|
extend Forwardable
|
||
|
|
||
|
INTERFACE = %w{
|
||
|
pk
|
||
|
associations
|
||
|
id
|
||
|
id=
|
||
|
name
|
||
|
description
|
||
|
description=
|
||
|
user_id
|
||
|
user_id=
|
||
|
[]
|
||
|
[]=
|
||
|
new_record?
|
||
|
save
|
||
|
save_changes
|
||
|
map_id
|
||
|
map_id=
|
||
|
valid?
|
||
|
table_id
|
||
|
table_id=
|
||
|
map
|
||
|
tags
|
||
|
tags=
|
||
|
set_tag_array
|
||
|
data_import_id
|
||
|
data_import_id=
|
||
|
user_id
|
||
|
user_id=
|
||
|
updated_at
|
||
|
private?
|
||
|
public?
|
||
|
public_with_link_only?
|
||
|
privacy
|
||
|
privacy=
|
||
|
privacy_changed?
|
||
|
privacy_text
|
||
|
destroy
|
||
|
errors
|
||
|
set_except
|
||
|
update_updated_at
|
||
|
values
|
||
|
affected_visualizations
|
||
|
fully_dependent_visualizations
|
||
|
partially_dependent_visualizations
|
||
|
dependent_visualizations
|
||
|
reload
|
||
|
}
|
||
|
|
||
|
PRIVACY_PRIVATE = 0
|
||
|
PRIVACY_PUBLIC = 1
|
||
|
PRIVACY_LINK = 2
|
||
|
|
||
|
PRIVACY_VALUES_TO_TEXTS = {
|
||
|
PRIVACY_PRIVATE => 'private',
|
||
|
PRIVACY_PUBLIC => 'public',
|
||
|
PRIVACY_LINK => 'link'
|
||
|
}
|
||
|
|
||
|
# For compatibility with AR model
|
||
|
def new_record?
|
||
|
new?
|
||
|
end
|
||
|
|
||
|
# Associations
|
||
|
many_to_one :map
|
||
|
many_to_many :layers, join_table: :layers_user_tables,
|
||
|
left_key: :user_table_id,
|
||
|
right_key: :layer_id,
|
||
|
reciprocal: :user_tables
|
||
|
one_to_many :geocodings, key: :table_id
|
||
|
many_to_one :data_import, key: :data_import_id
|
||
|
many_to_one :user
|
||
|
|
||
|
plugin :association_dependencies, map: :destroy,
|
||
|
layers: :nullify
|
||
|
plugin :dirty
|
||
|
|
||
|
def_delegators :relator, :affected_visualizations, :synchronization
|
||
|
|
||
|
# Ignore mass-asigment on not allowed columns
|
||
|
self.strict_param_setting = false
|
||
|
|
||
|
# Allowed columns
|
||
|
set_allowed_columns(:privacy, :tags, :description)
|
||
|
|
||
|
# The service should take care of all hooks
|
||
|
def set_service(table_obj)
|
||
|
@service = table_obj
|
||
|
end
|
||
|
|
||
|
# Lazy initialization of service if not present
|
||
|
def service
|
||
|
@service ||= ::Table.new(user_table: self)
|
||
|
end
|
||
|
|
||
|
def sync_table_id
|
||
|
self.table_id = service.get_table_id
|
||
|
end
|
||
|
|
||
|
def self.find_by_identifier(user_id, identifier)
|
||
|
col = 'name'
|
||
|
|
||
|
table = fetch(%Q{
|
||
|
SELECT *
|
||
|
FROM user_tables
|
||
|
WHERE user_tables.user_id = ?
|
||
|
AND user_tables.#{col} = ?},
|
||
|
user_id, identifier
|
||
|
).first
|
||
|
raise RecordNotFound if table.nil?
|
||
|
table
|
||
|
end
|
||
|
|
||
|
def self.from_map_id_key(map_id)
|
||
|
"UserTable:from_map_id:#{map_id}"
|
||
|
end
|
||
|
|
||
|
def self.from_map_id(map_id)
|
||
|
key = self.from_map_id_key(map_id)
|
||
|
user_table = PerRequestSequelCache.get(key)
|
||
|
if user_table.nil?
|
||
|
user_table = self[map_id: map_id]
|
||
|
PerRequestSequelCache.set(key, user_table, nil) unless user_table.nil?
|
||
|
end
|
||
|
user_table
|
||
|
end
|
||
|
|
||
|
|
||
|
|
||
|
# Hooks definition -----------------------------------------------------------
|
||
|
def validate
|
||
|
super
|
||
|
|
||
|
# userid and table name tuple must be unique
|
||
|
validates_unique [:name, :user_id], message: 'is already taken'
|
||
|
|
||
|
# tables must have a user
|
||
|
errors.add(:user_id, "can't be blank") if user_id.blank?
|
||
|
|
||
|
errors.add(:user, "Viewer users can't create tables") if user && user.viewer
|
||
|
|
||
|
if Carto::DB::Sanitize::RESERVED_TABLE_NAMES.include?(name)
|
||
|
errors.add(:name, 'is a reserved keyword, please choose a different one')
|
||
|
end
|
||
|
|
||
|
# TODO this kind of check should be moved to the DB
|
||
|
# privacy setting must be a sane value
|
||
|
if privacy != PRIVACY_PRIVATE && privacy != PRIVACY_PUBLIC && privacy != PRIVACY_LINK
|
||
|
errors.add(:privacy, "has an invalid value '#{privacy}'")
|
||
|
end
|
||
|
|
||
|
unless user.try(:private_tables_enabled)
|
||
|
# If it's a new table and the user is trying to make it private
|
||
|
if new? && privacy == PRIVACY_PRIVATE
|
||
|
errors.add(:privacy, 'unauthorized to create private tables')
|
||
|
end
|
||
|
|
||
|
# if the table exists, is private, but the owner no longer has private privileges
|
||
|
if !new? && privacy == PRIVACY_PRIVATE && changed_columns.include?(:privacy)
|
||
|
errors.add(:privacy, 'unauthorized to modify privacy status to private')
|
||
|
end
|
||
|
|
||
|
# cannot change any existing table to 'with link'
|
||
|
if !new? && privacy == PRIVACY_LINK && changed_columns.include?(:privacy)
|
||
|
errors.add(:privacy, 'unauthorized to modify privacy status to public with link')
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def before_validation
|
||
|
set_default_table_privacy
|
||
|
super
|
||
|
end
|
||
|
|
||
|
def before_create
|
||
|
super
|
||
|
update_updated_at # TODO move to a DB trigger
|
||
|
service.before_create
|
||
|
end
|
||
|
|
||
|
def after_create
|
||
|
super
|
||
|
create_default_map_and_layers
|
||
|
create_default_visualization
|
||
|
set_default_table_privacy
|
||
|
save
|
||
|
|
||
|
service.after_create
|
||
|
end
|
||
|
|
||
|
def before_save
|
||
|
super
|
||
|
service.before_save
|
||
|
end
|
||
|
|
||
|
def after_save
|
||
|
super
|
||
|
service.after_save
|
||
|
end
|
||
|
|
||
|
def before_destroy
|
||
|
raise CartoDB::InvalidMember.new(user: "Viewer users can't destroy tables") if user && user.viewer
|
||
|
|
||
|
@table_visualization = table_visualization
|
||
|
@fully_dependent_visualizations_cache = fully_dependent_visualizations.to_a
|
||
|
@partially_dependent_visualizations_cache = partially_dependent_visualizations.to_a
|
||
|
|
||
|
super
|
||
|
end
|
||
|
|
||
|
def after_destroy
|
||
|
@table_visualization.delete_from_table if @table_visualization
|
||
|
@fully_dependent_visualizations_cache.each(&:delete)
|
||
|
@partially_dependent_visualizations_cache.each do |visualization|
|
||
|
visualization.unlink_from(self)
|
||
|
end
|
||
|
synchronization.delete if synchronization
|
||
|
|
||
|
service.after_destroy
|
||
|
super
|
||
|
end
|
||
|
|
||
|
def before_update
|
||
|
PerRequestSequelCache.delete(self.class.from_map_id_key(self.map_id))
|
||
|
super
|
||
|
end
|
||
|
|
||
|
def delete
|
||
|
PerRequestSequelCache.delete(self.class.from_map_id_key(self.map_id))
|
||
|
super
|
||
|
end
|
||
|
|
||
|
# --------------------------------------------------------------------------------
|
||
|
|
||
|
def update_cdb_tablemetadata
|
||
|
service.update_cdb_tablemetadata
|
||
|
end
|
||
|
|
||
|
def privacy_text
|
||
|
PRIVACY_VALUES_TO_TEXTS[self.privacy].upcase
|
||
|
end
|
||
|
|
||
|
# TODO move privacy to value object
|
||
|
# enforce standard format for this field
|
||
|
def privacy=(value)
|
||
|
case value
|
||
|
when 'PUBLIC', PRIVACY_PUBLIC, PRIVACY_PUBLIC.to_s
|
||
|
self[:privacy] = PRIVACY_PUBLIC
|
||
|
when 'LINK', PRIVACY_LINK, PRIVACY_LINK.to_s
|
||
|
self[:privacy] = PRIVACY_LINK
|
||
|
when 'PRIVATE', PRIVACY_PRIVATE, PRIVACY_PRIVATE.to_s
|
||
|
self[:privacy] = PRIVACY_PRIVATE
|
||
|
else
|
||
|
raise "Invalid privacy value '#{value}'"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def private?
|
||
|
self.privacy == PRIVACY_PRIVATE
|
||
|
end #private?
|
||
|
|
||
|
def public?
|
||
|
self.privacy == PRIVACY_PUBLIC
|
||
|
end #public?
|
||
|
|
||
|
def public_with_link_only?
|
||
|
self.privacy == PRIVACY_LINK
|
||
|
end #public_with_link_only?
|
||
|
|
||
|
# TODO move tags to value object. A set is more appropriate
|
||
|
def tags=(value)
|
||
|
return unless value
|
||
|
set_tag_array(value.split(','))
|
||
|
end
|
||
|
|
||
|
def set_tag_array(tag_array)
|
||
|
return unless tag_array
|
||
|
self[:tags] = tag_array.map{ |t| t.strip }.compact.delete_if{ |t| t.blank? }.uniq.join(',')
|
||
|
end
|
||
|
|
||
|
# Needed by syncs
|
||
|
def update_updated_at
|
||
|
self.updated_at = Time.now
|
||
|
end
|
||
|
|
||
|
def estimated_row_count
|
||
|
service.estimated_row_count
|
||
|
end
|
||
|
|
||
|
def actual_row_count
|
||
|
service.actual_row_count
|
||
|
end
|
||
|
|
||
|
def external_source_visualization
|
||
|
if data_import_id
|
||
|
Carto::ExternalDataImports.where(data_import_id: data_import_id)&.first&.external_source&.visualization
|
||
|
else
|
||
|
nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def table_visualization
|
||
|
@table_visualization ||= map_id && CartoDB::Visualization::Collection.new.fetch(
|
||
|
map_id: map_id,
|
||
|
type: CartoDB::Visualization::Member::TYPE_CANONICAL
|
||
|
).first
|
||
|
end
|
||
|
|
||
|
def privacy_changed?
|
||
|
previous_changes && previous_changes.keys.include?(:privacy)
|
||
|
end
|
||
|
|
||
|
def privacy_was
|
||
|
previous_changes[:privacy].first
|
||
|
end
|
||
|
|
||
|
def fully_dependent_visualizations
|
||
|
affected_visualizations.select { |v| v.fully_dependent_on?(self) }
|
||
|
end
|
||
|
|
||
|
def partially_dependent_visualizations
|
||
|
affected_visualizations.select { |v| v.partially_dependent_on?(self) }
|
||
|
end
|
||
|
|
||
|
def dependent_visualizations
|
||
|
affected_visualizations.select { |v| v.dependent_on?(self) }
|
||
|
end
|
||
|
|
||
|
def is_owner?(user)
|
||
|
return false unless user
|
||
|
user_id == user.id
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def default_privacy_value
|
||
|
user.try(:private_tables_enabled) ? PRIVACY_PRIVATE : PRIVACY_PUBLIC
|
||
|
end
|
||
|
|
||
|
def set_default_table_privacy
|
||
|
self.privacy ||= default_privacy_value
|
||
|
end
|
||
|
|
||
|
def create_default_map_and_layers
|
||
|
base_layer = ::ModelFactories::LayerFactory.get_default_base_layer(user)
|
||
|
|
||
|
self.map = ::ModelFactories::MapFactory.get_map(base_layer, user.id, id)
|
||
|
map.add_layer(base_layer)
|
||
|
|
||
|
geometry_type = service.the_geom_type || 'geometry'
|
||
|
data_layer = ::ModelFactories::LayerFactory.get_default_data_layer(name, user, geometry_type)
|
||
|
|
||
|
map.add_layer(data_layer)
|
||
|
|
||
|
if base_layer.supports_labels_layer?
|
||
|
labels_layer = ::ModelFactories::LayerFactory.get_default_labels_layer(base_layer)
|
||
|
map.add_layer(labels_layer)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def create_default_visualization
|
||
|
kind = service.is_raster? ? CartoDB::Visualization::Member::KIND_RASTER : CartoDB::Visualization::Member::KIND_GEOM
|
||
|
|
||
|
esv = external_source_visualization
|
||
|
|
||
|
member = CartoDB::Visualization::Member.new(
|
||
|
name: name,
|
||
|
map_id: map.id,
|
||
|
type: CartoDB::Visualization::Member::TYPE_CANONICAL,
|
||
|
description: description,
|
||
|
attributions: esv.try(:attributions),
|
||
|
source: esv.try(:source),
|
||
|
tags: (tags.split(',') if tags),
|
||
|
privacy: UserTable::PRIVACY_VALUES_TO_TEXTS[default_privacy_value],
|
||
|
user_id: user.id,
|
||
|
kind: kind
|
||
|
)
|
||
|
|
||
|
member.store
|
||
|
member.map.set_default_boundaries!
|
||
|
map.reload
|
||
|
|
||
|
CartoDB::Visualization::Overlays.new(member).create_default_overlays
|
||
|
end
|
||
|
|
||
|
def relator
|
||
|
@relator ||= CartoDB::TableRelator.new(SequelRails.connection, self)
|
||
|
end
|
||
|
end
|