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