Initial commit
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
require_dependency 'carto/uuidhelper'
|
||||
require_relative '../builder/builder_users_module'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class AnalysesController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
include Carto::UUIDHelper
|
||||
include Carto::Builder::BuilderUsersModule
|
||||
|
||||
ssl_required :show, :create, :update, :destroy
|
||||
|
||||
before_filter :builder_users_only
|
||||
before_filter :load_visualization
|
||||
before_filter :check_user_can_add_analysis, only: [:show, :create, :update, :destroy]
|
||||
before_filter :load_analysis, only: [:show, :update, :destroy]
|
||||
|
||||
rescue_from StandardError, with: :rescue_from_standard_error
|
||||
rescue_from Carto::LoadError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UnauthorizedError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
|
||||
def show
|
||||
render_jsonp(AnalysisPresenter.new(@analysis).to_poro)
|
||||
end
|
||||
|
||||
def create
|
||||
natural_id = analysis_definition_from_request['id']
|
||||
|
||||
analysis = Carto::Analysis.find_by_natural_id(@visualization.id, natural_id)
|
||||
if analysis
|
||||
analysis.analysis_definition = analysis_definition_from_request
|
||||
else
|
||||
analysis = Carto::Analysis.new(
|
||||
visualization_id: @visualization.id,
|
||||
user_id: current_user.id,
|
||||
analysis_definition: analysis_definition_from_request
|
||||
)
|
||||
end
|
||||
analysis.save!
|
||||
render_jsonp(AnalysisPresenter.new(analysis).to_poro, 201)
|
||||
end
|
||||
|
||||
def update
|
||||
new_definition = analysis_definition_from_request
|
||||
new_root_node = Carto::AnalysisNode.new(new_definition.deep_symbolize_keys)
|
||||
modified_node_ids = find_modified_nodes(@analysis.analysis_node, new_root_node)
|
||||
affected_node_ids = find_affected_nodes(modified_node_ids)
|
||||
|
||||
@analysis.analysis_definition = new_definition
|
||||
@analysis.save!
|
||||
purge_layer_node_style_history(affected_node_ids)
|
||||
|
||||
render_jsonp(AnalysisPresenter.new(@analysis).to_poro, 200)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@analysis.destroy
|
||||
render_jsonp(AnalysisPresenter.new(@analysis).to_poro, 200)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def purge_layer_node_style_history(node_ids)
|
||||
Carto::LayerNodeStyle.from_visualization_and_source(@visualization, node_ids).delete_all
|
||||
end
|
||||
|
||||
def find_affected_nodes(modified_node_ids)
|
||||
all_visualization_nodes = @visualization.analyses.map(&:analysis_node).map(&:descendants).flatten
|
||||
all_visualization_nodes.select { |node|
|
||||
node.descendants.any? { |descendant| modified_node_ids.include?(descendant.id) }
|
||||
}.map(&:id)
|
||||
end
|
||||
|
||||
def find_modified_nodes(old_root, new_root)
|
||||
old_nodes = old_root.descendants
|
||||
new_nodes = new_root.descendants
|
||||
|
||||
old_ids = old_nodes.map(&:id)
|
||||
new_ids = new_nodes.map(&:id)
|
||||
|
||||
kept_ids = old_ids & new_ids
|
||||
kept_ids.select do |node_id|
|
||||
old_node = old_nodes.find { |n| n.id == node_id }
|
||||
new_node = new_nodes.find { |n| n.id == node_id }
|
||||
old_node.non_child_params != new_node.non_child_params
|
||||
end
|
||||
end
|
||||
|
||||
def analysis_definition_from_request
|
||||
analysis_json = json_post(request.raw_post)
|
||||
|
||||
if analysis_json.nil? || analysis_json.empty?
|
||||
raise Carto::UnprocesableEntityError.new("Empty analysis")
|
||||
end
|
||||
|
||||
analysis_definition = analysis_json['analysis_definition']
|
||||
if analysis_definition.nil? || analysis_definition.empty? || analysis_definition.class == String
|
||||
raise Carto::UnprocesableEntityError.new("Invalid analysis definition")
|
||||
end
|
||||
|
||||
analysis_definition
|
||||
end
|
||||
|
||||
def json_post(raw_post = request.raw_post)
|
||||
@json_post ||= (raw_post.present? ? JSON.parse(raw_post) : nil)
|
||||
rescue => e
|
||||
# Malformed JSON is not our business
|
||||
CartoDB.notify_warning_exception(e)
|
||||
raise UnprocesableEntityError.new("Malformed JSON: #{raw_post}")
|
||||
end
|
||||
|
||||
def load_visualization
|
||||
visualization_id = params[:visualization_id]
|
||||
|
||||
if payload_visualization_id.present? && payload_visualization_id != visualization_id
|
||||
raise UnprocesableEntityError.new("url vis (#{visualization_id}) != payload (#{payload_visualization_id})")
|
||||
end
|
||||
|
||||
@visualization = Carto::Visualization.where(id: visualization_id).first if visualization_id
|
||||
raise LoadError.new("Visualization not found: #{visualization_id}") unless @visualization
|
||||
end
|
||||
|
||||
def payload_visualization_id
|
||||
json_post.present? ? json_post['visualization_id'] : nil
|
||||
end
|
||||
|
||||
def payload_analysis_id
|
||||
json_post.present? ? json_post['id'] : nil
|
||||
end
|
||||
|
||||
def check_user_can_add_analysis
|
||||
if @visualization.user_id != current_user.id
|
||||
raise Carto::UnauthorizedError.new("#{current_user.id} doesn't own visualization #{@visualization.id}")
|
||||
end
|
||||
end
|
||||
|
||||
def load_analysis
|
||||
if payload_analysis_id.present? && payload_analysis_id != params[:id]
|
||||
raise UnprocesableEntityError.new("url analysis (#{params[:id]}) != payload (#{payload_analysis_id})")
|
||||
end
|
||||
|
||||
unless params[:id].nil?
|
||||
@analysis = Carto::Analysis.where(id: params[:id]).first if is_uuid?(params[:id])
|
||||
|
||||
if @analysis.nil?
|
||||
@analysis = Carto::Analysis.find_by_natural_id(@visualization.id, params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
raise Carto::LoadError.new("Analysis not found: #{params[:id]}") unless @analysis
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
module Carto
|
||||
module Api
|
||||
class AnalysisPresenter
|
||||
|
||||
def initialize(analysis)
|
||||
@analysis = analysis
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return {} unless @analysis
|
||||
|
||||
@analysis.analysis_node.descendants.each do |node|
|
||||
style_history = Carto::LayerNodeStyle.from_visualization_and_source(@analysis.visualization, node.id)
|
||||
node.options[:style_history] = style_history.map { |lns|
|
||||
[
|
||||
lns.layer_id,
|
||||
{
|
||||
tooltip: lns.tooltip,
|
||||
infowindow: lns.infowindow,
|
||||
options: lns.options
|
||||
}
|
||||
]
|
||||
}.to_h
|
||||
end
|
||||
|
||||
{
|
||||
id: @analysis.id,
|
||||
analysis_definition: @analysis.analysis_definition
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
require 'json'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class ApiKeyPresenter
|
||||
def initialize(api_key)
|
||||
@api_key = api_key
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return {} unless @api_key
|
||||
|
||||
{
|
||||
name: @api_key.name,
|
||||
user: { username: @api_key.user.username },
|
||||
type: @api_key.type,
|
||||
token: @api_key.token,
|
||||
grants: get_grants,
|
||||
created_at: @api_key.created_at.to_s,
|
||||
updated_at: @api_key.updated_at.to_s
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_grants
|
||||
grants = [
|
||||
{
|
||||
type: 'apis',
|
||||
apis: @api_key.granted_apis
|
||||
}
|
||||
]
|
||||
|
||||
type_database = {
|
||||
type: 'database',
|
||||
tables: table_permissions_for_api_key,
|
||||
schemas: schema_permissions_for_api_key
|
||||
}
|
||||
|
||||
if @api_key.dataset_metadata_permissions
|
||||
type_database['table_metadata'] = []
|
||||
end
|
||||
|
||||
grants << type_database
|
||||
|
||||
if @api_key.data_services?
|
||||
grants << {
|
||||
type: 'dataservices',
|
||||
services: @api_key.data_services
|
||||
}
|
||||
end
|
||||
|
||||
grants
|
||||
end
|
||||
|
||||
def table_permissions_for_api_key
|
||||
return [] if @api_key.master? || @api_key.default_public?
|
||||
|
||||
@api_key.table_permissions_from_db.map do |p|
|
||||
{
|
||||
schema: p.schema,
|
||||
name: p.name,
|
||||
owner: p.owner,
|
||||
permissions: p.permissions
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def schema_permissions_for_api_key
|
||||
return [] if @api_key.master? || @api_key.default_public?
|
||||
|
||||
@api_key.schema_permissions_from_db.map do |p|
|
||||
{
|
||||
name: p.name,
|
||||
permissions: p.permissions
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
require_relative 'paged_searcher'
|
||||
|
||||
class Carto::Api::ApiKeysController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
include Carto::UUIDHelper
|
||||
include Carto::Api::PagedSearcher
|
||||
include Carto::Api::AuthApiAuthentication
|
||||
|
||||
ssl_required :create, :destroy, :regenerate_token, :show, :index
|
||||
|
||||
before_filter :any_api_authorization_required, only: [:index, :show]
|
||||
skip_filter :api_authorization_required, only: [:index, :show]
|
||||
before_filter :engine_required
|
||||
before_filter :load_api_key, only: [:destroy, :regenerate_token, :show]
|
||||
|
||||
rescue_from Carto::ParamInvalidError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::LoadError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UnauthorizedError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::CartoError, with: :rescue_from_carto_error
|
||||
|
||||
VALID_ORDER_PARAMS = [:type, :name, :updated_at].freeze
|
||||
VALID_TYPE_PARAMS = [Carto::ApiKey::TYPE_MASTER,
|
||||
Carto::ApiKey::TYPE_DEFAULT_PUBLIC,
|
||||
Carto::ApiKey::TYPE_REGULAR].freeze
|
||||
|
||||
def create
|
||||
carto_viewer = Carto::User.find(current_viewer.id)
|
||||
api_key = carto_viewer.api_keys.create_regular_key!(name: params[:name], grants: params[:grants])
|
||||
render_jsonp(Carto::Api::ApiKeyPresenter.new(api_key).to_poro, 201)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
raise Carto::UnprocesableEntityError.new(e.message)
|
||||
rescue CartoDB::QuotaExceeded => e
|
||||
raise Carto::CartoError.new(e.message, 403)
|
||||
end
|
||||
|
||||
def destroy
|
||||
raise Carto::UnauthorizedError.new unless @viewed_api_key.can_be_deleted?
|
||||
|
||||
@viewed_api_key.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def regenerate_token
|
||||
@viewed_api_key.regenerate_token!
|
||||
render_jsonp(Carto::Api::ApiKeyPresenter.new(@viewed_api_key).to_poro, 200)
|
||||
end
|
||||
|
||||
def index
|
||||
page, per_page, order, _order_direction = page_per_page_order_params(VALID_ORDER_PARAMS)
|
||||
|
||||
api_keys = Carto::User.find(current_user.id).api_keys.by_type(type_param).order_weighted_by_type
|
||||
api_keys = request_api_key.master? ? api_keys : api_keys.where(id: request_api_key.id)
|
||||
filtered_api_keys = Carto::PagedModel.paged_association(api_keys, page, per_page, order)
|
||||
|
||||
result = filtered_api_keys.map { |api_key| json_for_api_key(api_key) }
|
||||
|
||||
render_jsonp(
|
||||
paged_result(
|
||||
result: result,
|
||||
total_count: api_keys.count,
|
||||
page: page,
|
||||
per_page: per_page,
|
||||
params: params
|
||||
) { |params| api_keys_url(params) },
|
||||
200
|
||||
)
|
||||
end
|
||||
|
||||
def show
|
||||
render_jsonp(Carto::Api::ApiKeyPresenter.new(@viewed_api_key).to_poro, 200)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_api_key
|
||||
name = params[:id]
|
||||
@viewed_api_key = Carto::ApiKey.where(user_id: current_viewer.id, name: name).user_visible.first
|
||||
if !@viewed_api_key || !request_api_key.master? && @viewed_api_key != request_api_key
|
||||
raise Carto::LoadError.new("API key not found: #{name}")
|
||||
end
|
||||
end
|
||||
|
||||
def json_for_api_key(api_key)
|
||||
Carto::Api::ApiKeyPresenter.new(api_key).to_poro.merge(
|
||||
_links: {
|
||||
self: api_key_url(id: CGI::escape(api_key.name))
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def type_param
|
||||
types = (params[:type] || '').split(',').map(&:strip)
|
||||
raise Carto::ParamInvalidError.new(:type, VALID_TYPE_PARAMS) unless (types - VALID_TYPE_PARAMS).empty?
|
||||
types
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
class Carto::Api::AssetPresenter
|
||||
def initialize(asset)
|
||||
@asset = asset
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
id: @asset.id,
|
||||
public_url: @asset.absolute_public_url,
|
||||
user_id: @asset.user_id,
|
||||
kind: @asset.kind
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
class Carto::Api::AssetsController < ::Api::ApplicationController
|
||||
ssl_required :index
|
||||
|
||||
def index
|
||||
assets = current_viewer.assets.map do |asset|
|
||||
Carto::Api::AssetPresenter.new(asset).to_hash
|
||||
end
|
||||
|
||||
render json: { total_entries: assets.size, assets: assets }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
require_relative '../../../models/carto/permission'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class ColumnsController < ::Api::ApplicationController
|
||||
|
||||
ssl_required :index, :show, :create, :update, :destroy
|
||||
|
||||
before_filter :load_user_table, only: [:index, :show, :create, :update, :destroy]
|
||||
before_filter :read_privileges?, only: [:index, :show]
|
||||
before_filter :write_privileges?, only: [:create, :update, :destroy]
|
||||
|
||||
def index
|
||||
render_jsonp(@user_table.service.schema(cartodb_types: true))
|
||||
end
|
||||
|
||||
def show
|
||||
resp = @user_table.service.schema(cartodb_types: true).select { |e| e[0] == params[:id].to_sym }.first.last
|
||||
render_jsonp(type: resp)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(message: 'Error loading column', exception: e,
|
||||
column_id: params[:id], user_table: @user_table)
|
||||
render_jsonp({ errors: "Column #{params[:id]} doesn't exist" }, 404)
|
||||
end
|
||||
|
||||
def create
|
||||
render_jsonp(@user_table.service.add_column!(params.slice(:type, :name)))
|
||||
rescue => e
|
||||
errors = e.is_a?(CartoDB::InvalidType) ? [e.db_message] : [translate_error(e.message.split("\n").first)]
|
||||
render_jsonp({ errors: errors }, 400)
|
||||
end
|
||||
|
||||
def update
|
||||
render_jsonp(@user_table.service.modify_column!(name: params[:id],
|
||||
type: params[:type],
|
||||
new_name: params[:new_name]))
|
||||
rescue => e
|
||||
errors = e.is_a?(CartoDB::InvalidType) ? [e.db_message] : [translate_error(e.message.split("\n").first)]
|
||||
render_jsonp({ errors: errors }, 400)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user_table.service.drop_column!(name: params[:id])
|
||||
|
||||
head :no_content
|
||||
rescue => e
|
||||
errors = e.is_a?(CartoDB::InvalidType) ? [e.db_message] : [translate_error(e.message.split("\n").first)]
|
||||
render_jsonp({ errors: errors }, 400)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def load_user_table
|
||||
@user_table = Carto::Helpers::TableLocator.new.get_by_id_or_name(params[:table_id], current_user)
|
||||
raise RecordNotFound unless @user_table
|
||||
end
|
||||
|
||||
def read_privileges?
|
||||
head(401) unless current_user && @user_table.visualization.is_viewable_by_user?(current_user)
|
||||
end
|
||||
|
||||
def write_privileges?
|
||||
head(401) unless current_user && @user_table.visualization.writable_by?(current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
module Carto
|
||||
module Api
|
||||
class ConnectorsController < ::Api::ApplicationController
|
||||
|
||||
MAX_LISTED_TABLES = 500
|
||||
|
||||
ssl_required :index, :show, :tables, :connect
|
||||
|
||||
before_filter :check_availability
|
||||
|
||||
def index
|
||||
render_jsonp(Carto::Connector.providers(user: current_user))
|
||||
end
|
||||
|
||||
def show
|
||||
provider_id = params[:provider_id]
|
||||
begin
|
||||
information = Carto::Connector.information(provider_id)
|
||||
render_jsonp(information)
|
||||
rescue Carto::Connector::InvalidParametersError
|
||||
render_jsonp({ errors: "Provider #{provider_id} unknown" }, 422)
|
||||
end
|
||||
end
|
||||
|
||||
def connect
|
||||
provider_id = params[:provider_id]
|
||||
parameters = build_connection_parameters(provider_id, params)
|
||||
begin
|
||||
connector = Carto::Connector.new(parameters, user: current_user, logger: nil)
|
||||
render_jsonp({"connected": connector.check_connection})
|
||||
rescue Carto::Connector::InvalidParametersError => e
|
||||
render_jsonp({ errors: e.message }, 422)
|
||||
rescue
|
||||
render_jsonp({ errors: "Error connecting to provider #{provider_id}, check connection parameters" }, 400)
|
||||
end
|
||||
end
|
||||
|
||||
def tables
|
||||
provider_id = params[:provider_id]
|
||||
parameters = build_connection_parameters(provider_id, params)
|
||||
if Carto::Connector.list_tables?(provider_id)
|
||||
begin
|
||||
connector = Carto::Connector.new(parameters, user: current_user, logger: nil)
|
||||
render_jsonp(connector.list_tables(MAX_LISTED_TABLES))
|
||||
rescue Carto::Connector::InvalidParametersError => e
|
||||
render_jsonp({ errors: e.message }, 422)
|
||||
rescue
|
||||
render_jsonp({ errors: "Error connecting to provider #{provider_id}, check connection parameters" }, 400)
|
||||
end
|
||||
else
|
||||
render_jsonp({ errors: "Provider #{provider_id} doesn't support list tables" }, 422)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_connection_parameters(provider_id, request_params)
|
||||
parameters = {}
|
||||
parameters[:provider] = request_params[:provider_id]
|
||||
parameters[:connection] = {}
|
||||
provider_information = Carto::Connector.information(provider_id)
|
||||
provider_information[:parameters]["connection"].each do |key, _value|
|
||||
if request_params[key.to_sym].present?
|
||||
parameters[:connection][key.to_sym] = request_params[key.to_sym]
|
||||
end
|
||||
end
|
||||
parameters
|
||||
end
|
||||
|
||||
def check_availability
|
||||
head 404 unless Connector.available?(current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,191 @@
|
||||
require 'uri'
|
||||
require 'json'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class DataImportPresenter
|
||||
|
||||
HTTP_RESPONSE_CODE_MESSAGES = {
|
||||
"300" => "This usually means the file has been moved! Click on the link to get its new location.",
|
||||
"301" => "This usually means the file has been moved! Click on the link to get its new location.",
|
||||
"302" => "This usually means the file has been temporally moved! Click on the link to get its new location.",
|
||||
"303" => "This usually means the file has been moved! Click on the link to get its new location.",
|
||||
"307" => "This usually means the file has been temporally moved! Click on the link to get its new location.",
|
||||
"400" => "This usually means CARTO did not perform the request properly.",
|
||||
"401" => "This usually means CARTO is not authorized to retrieve this file. If you have authorization, " \
|
||||
"download the file manually and upload it from your computer.",
|
||||
"402" => "This usually means CARTO is not authorized to retrieve this file. If you have authorization, " \
|
||||
"download the file manually and upload it from your computer.",
|
||||
"403" => "This usually means CARTO is not authorized to retrieve this file. If you have authorization, " \
|
||||
"download the file manually and upload it from your computer. Tip: Should you be using 'https' " \
|
||||
"instead of 'http' in your URL?",
|
||||
"404" => "This usually means no file was found at the specified URL.",
|
||||
"405" => "This usually means CARTO could not negotiate the download with the file's provider. The " \
|
||||
"provider is probably using a non standard method to serve this file.",
|
||||
"407" => "This usually means CARTO is not authorized to retrieve this file as proxy authentication " \
|
||||
"is required. If you can use the apropiate proxy, download the file manually and upload " \
|
||||
"it from your computer.",
|
||||
"408" => "This usually means a timeout request was produced. You may want to try again.",
|
||||
"409" => "This usually means a confilict in the request was produced. You might want to try again.",
|
||||
"410" => "This usually means the file specified is now longer available at this location.",
|
||||
"411" => "This usually means CARTO did not perform the request properly. 'Content Length' header is missing.",
|
||||
"412" => "This usually means CARTO could not negotiate the download with the file's provider. The " \
|
||||
"provider is probably using a non standard method to serve this file.",
|
||||
"413" => "This usually means the file provider is denying the download because the file is too large.",
|
||||
"417" => "This usually means CARTO could not negotiate the download with the file's provider. The " \
|
||||
"provider is probably using a non standard method to serve this file.",
|
||||
"500" => "This usually means the file provider responded with an internal server error. They might " \
|
||||
"be overloaded or having some down time. Try again later!",
|
||||
"501" => "This usually means CARTO could not negotiate the download with the file's provider. The " \
|
||||
"provider is probably using a non standard method to serve this file.",
|
||||
"502" => "This usually means the file provider responded with a bad gateway error.",
|
||||
"503" => "This usually means the file provider responded with an internal server error. They might " \
|
||||
"be overloaded or having some down time. Try again later!",
|
||||
"505" => "This usually means the file provider doesn't seem to support the HTTP version used in the " \
|
||||
"transaction. The provider is probably using a non standard method to serve this file.",
|
||||
"511" => "This usually means CARTO is not authorized to retrieve this file. If you have " \
|
||||
"authorization, download the file manually and upload it from your computer." }
|
||||
|
||||
def initialize(data_import)
|
||||
@data_import = data_import
|
||||
end
|
||||
|
||||
def api_public_values
|
||||
public_values.reject { |key|
|
||||
NON_API_VISIBLE_ATTRIBUTES.include?(key)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
PUBLIC_ATTRIBUTES = [
|
||||
'id',
|
||||
'user_id',
|
||||
'table_id',
|
||||
'data_type',
|
||||
'table_name',
|
||||
'state',
|
||||
'error_code',
|
||||
'queue_id',
|
||||
'tables_created_count',
|
||||
'synchronization_id',
|
||||
'service_name',
|
||||
'service_item_id',
|
||||
'type_guessing',
|
||||
'quoted_fields_guessing',
|
||||
'content_guessing',
|
||||
'server',
|
||||
'host',
|
||||
'upload_host',
|
||||
'resque_ppid',
|
||||
'create_visualization',
|
||||
'visualization_id',
|
||||
# String field containing a json, format:
|
||||
# {
|
||||
# twitter_credits: Integer
|
||||
# }
|
||||
# No automatic conversion coded
|
||||
'user_defined_limits'
|
||||
]
|
||||
|
||||
NON_API_VISIBLE_ATTRIBUTES = [
|
||||
'service_item_id',
|
||||
'service_name',
|
||||
'server',
|
||||
'host',
|
||||
'upload_host',
|
||||
'resque_ppid',
|
||||
]
|
||||
|
||||
def public_values
|
||||
values = Hash[PUBLIC_ATTRIBUTES.map{ |attribute| [attribute, @data_import.send(attribute)] }]
|
||||
values.merge!('get_error_text' => get_error_text)
|
||||
values.merge!('display_name' => display_name)
|
||||
values.merge!('queue_id' => @data_import.id)
|
||||
values.merge!(success: @data_import.success) if @data_import.final_state?
|
||||
values.merge!(original_url: @data_import.original_url)
|
||||
values.merge!(data_type: @data_import.data_type)
|
||||
values.merge!(warnings: get_warnings)
|
||||
values.merge!(is_raster: @data_import.is_raster?)
|
||||
|
||||
if !@data_import.http_response_code.nil? && !@data_import.http_response_code.start_with?('2')
|
||||
values.merge!(http_response_code: @data_import.http_response_code)
|
||||
values.merge!(http_response_code_message: get_http_response_code_message(@data_import.http_response_code))
|
||||
end
|
||||
|
||||
values
|
||||
end
|
||||
|
||||
def get_error_text
|
||||
if @data_import.error_code.nil?
|
||||
nil
|
||||
else
|
||||
@data_import.error_code.blank? ? CartoDB::IMPORTER_ERROR_CODES[99999] : CartoDB::IMPORTER_ERROR_CODES[@data_import.error_code]
|
||||
end
|
||||
end
|
||||
|
||||
def display_name
|
||||
if @data_import.service_name == 'twitter_search'
|
||||
extract_twitter_display_name(@data_import)
|
||||
else
|
||||
url = [@data_import.data_source, @data_import.service_item_id].compact.select { |s| s != '' }.first
|
||||
display_name = url.nil? ? @data_import.id : extract_filename(url)
|
||||
display_name || @data_import.id
|
||||
end
|
||||
rescue => e
|
||||
CartoDB.notify_debug(
|
||||
'Error extracting display name',
|
||||
data_import_id: @data_import.id,
|
||||
service_item_id: @data_import.service_item_id,
|
||||
data_source: @data_import.data_source,
|
||||
exception: e.inspect
|
||||
)
|
||||
@data_import.id
|
||||
end
|
||||
|
||||
def extract_filename(url)
|
||||
URI.decode(File.basename(URI.parse(URI.encode(url.strip)).path))
|
||||
end
|
||||
|
||||
def extract_twitter_display_name(data_import)
|
||||
"Tweets about '#{JSON.parse(data_import.service_item_id)['categories'].map { |c| c['terms'] }.join(', ')}'"
|
||||
rescue => e
|
||||
CartoDB.notify_debug('Error extracting Twitter import display name', { data_import_id: data_import.id, service_item_id: data_import.service_item_id, data_source: data_import.data_source })
|
||||
"Twitter search #{data_import.id}"
|
||||
end
|
||||
|
||||
def get_http_response_code_message(http_response_code)
|
||||
return nil if http_response_code.nil?
|
||||
|
||||
message = HTTP_RESPONSE_CODE_MESSAGES[http_response_code]
|
||||
|
||||
if message.nil?
|
||||
message = case http_response_code
|
||||
when /^1/ then "This usually means more actions are required to download this file."
|
||||
when /^2/ then "This usually means everything went fine while fetching the file."
|
||||
when /^3/ then "This usually means an unknown redirection error has occured."
|
||||
when /^4/ then "This usually means an unknown client error has occured."
|
||||
when /^5/ then "This usually means an unknown server error has occured."
|
||||
else "This is an unkown type of HTTP status code."
|
||||
end
|
||||
end
|
||||
|
||||
message
|
||||
end
|
||||
|
||||
# All warnings should be parsed into a single Hash here
|
||||
def get_warnings
|
||||
warnings = {}
|
||||
|
||||
if !@data_import.rejected_layers.nil?
|
||||
warnings.merge!(rejected_layers: @data_import.rejected_layers.split(','))
|
||||
warnings.merge!(user_max_layers: @data_import.user.max_layers)
|
||||
end
|
||||
|
||||
warnings.merge!(JSON.parse(@data_import.runner_warnings)) if !@data_import.runner_warnings.nil?
|
||||
|
||||
warnings.empty? ? nil : warnings
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,197 @@
|
||||
require_dependency 'cartodb/errors'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
|
||||
# Group metadata registration. Exclusively for usage from PostgreSQL Extension, not from the Editor.
|
||||
# It only registers metadata, actual group roles management must be done by the extension.
|
||||
# Named "DatabaseGroups" because it receives _databases_, not organizations.
|
||||
class DatabaseGroupsController < ::ApplicationController
|
||||
|
||||
respond_to :json
|
||||
|
||||
ssl_required :create, :update, :destroy, :add_users, :remove_users, :update_permission, :destroy_permission
|
||||
|
||||
# TODO: Make this controller inherit from ::Api::ApplicationController and remove skip_before_filter bellow
|
||||
skip_before_filter :verify_authenticity_token
|
||||
|
||||
# Allow HTTPS on local/test as the calls from the groups API are done sending a https X-Forwarded-Proto,
|
||||
# like simulating they come from https:
|
||||
# @see https://github.com/CartoDB/cartodb-postgresql/blob/bce61c1e4359653134134097d269edae581e5660/scripts-available/CDB_Groups_API.sql#L170
|
||||
# Without it, SSL is forbidden and a 302 to url "without HTTP" was returned so the API didn't work on testing
|
||||
|
||||
def ssl_allowed?
|
||||
Rails.env.development? || Rails.env.test?
|
||||
end
|
||||
|
||||
before_filter :authenticate_extension
|
||||
before_filter :load_parameters
|
||||
before_filter :load_mandatory_group, :only => [:destroy, :add_users, :remove_users, :update_permission, :destroy_permission]
|
||||
before_filter :load_user_from_username, :only => [:load_table, :update_permission, :destroy_permission]
|
||||
before_filter :load_users_from_username, :only => [:add_users, :remove_users]
|
||||
before_filter :load_table, :only => [:update_permission, :destroy_permission]
|
||||
|
||||
def create
|
||||
group = Group.new_instance(@database_name, @name, @database_role)
|
||||
if group.save
|
||||
render json: group.to_json
|
||||
else
|
||||
render json: { errors: "Error saving group: #{group.errors}" }, status: 400
|
||||
end
|
||||
rescue CartoDB::ModelAlreadyExistsError => e
|
||||
CartoDB.notify_debug('Group already exists', { params: params })
|
||||
render json: { errors: "A group with that data already exists" }, status: 409
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { params: params , group: (group ? group : 'not created') })
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
def update
|
||||
new_name = params['name']
|
||||
group = get_group_from_loaded_parameters
|
||||
if group
|
||||
group.rename(new_name, @database_role)
|
||||
if group.save
|
||||
render json: @group.to_json
|
||||
else
|
||||
raise "Error saving group: #{@group.errors}"
|
||||
end
|
||||
else
|
||||
renamed_group = get_group(@database_name, new_name)
|
||||
if renamed_group && renamed_group.database_role == @database_role
|
||||
CartoDB.notify_debug('Group already renamed', { params: params })
|
||||
render json: { errors: "That group has already been renamed" }, status: 409
|
||||
else
|
||||
raise "Group not found and no matching rename found"
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
def destroy
|
||||
@group.destroy
|
||||
render json: {}, status: 204
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
def add_users
|
||||
added_usernames = []
|
||||
@usernames.map { |username|
|
||||
begin
|
||||
added_usernames << @group.add_user(username).user.username
|
||||
rescue CartoDB::ModelAlreadyExistsError => e
|
||||
# This will provoke 409 response later
|
||||
end
|
||||
}
|
||||
if added_usernames.length == @usernames.length
|
||||
render json: { users: added_usernames }, status: 200
|
||||
else
|
||||
render json: { errors: "Some users were already in the group: #{@usernames - added_usernames }", users: added_usernames }, status: 409
|
||||
end
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
def remove_users
|
||||
removed_usernames = []
|
||||
@usernames.map { |username|
|
||||
removed_user = @group.remove_user(username)
|
||||
removed_usernames << removed_user.username if !removed_user.nil?
|
||||
}
|
||||
if removed_usernames.length == @usernames.length
|
||||
render json: { users: removed_usernames }, status: 200
|
||||
else
|
||||
render json: { errors: "Some users (#{@usernames - removed_usernames}) were not in the group", users: removed_usernames }, status: 404
|
||||
end
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
def update_permission
|
||||
permission = CartoDB::Permission[@table.permission.id]
|
||||
permission.set_group_permission(@group, @access)
|
||||
permission.save
|
||||
render json: {}, status: 200
|
||||
rescue CartoDB::ModelAlreadyExistsError => e
|
||||
CartoDB.notify_debug('Permission already granted', { params: params })
|
||||
render json: { errors: "That permission is already granted" }, status: 409
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
def destroy_permission
|
||||
permission = CartoDB::Permission[@table.permission.id]
|
||||
permission.remove_group_permission(@group)
|
||||
permission.save
|
||||
render json: {}, status: 200
|
||||
rescue CartoDB::ModelAlreadyExistsError => e
|
||||
CartoDB.notify_debug('Permission already revoked', { params: params })
|
||||
render json: { errors: "That permission is already revoked" }, status: 404
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authenticate_extension
|
||||
raise "missing org_metadata_api configuration" unless Cartodb.config[:org_metadata_api]
|
||||
|
||||
authenticate_or_request_with_http_basic do |username, password|
|
||||
username == Cartodb.config[:org_metadata_api]["username"] && password == Cartodb.config[:org_metadata_api]["password"]
|
||||
end
|
||||
end
|
||||
|
||||
def load_parameters
|
||||
@database_name = params[:database_name]
|
||||
@name = [params[:old_name], params[:name]].compact.first
|
||||
@database_role = params[:database_role]
|
||||
@username = params[:username]
|
||||
@usernames = @username.present? ? [ @username ] : params[:users]
|
||||
@table_name = params[:table_name]
|
||||
case params['access']
|
||||
when nil
|
||||
when 'r'
|
||||
@access = CartoDB::Permission::ACCESS_READONLY
|
||||
when 'w'
|
||||
@access = CartoDB::Permission::ACCESS_READWRITE
|
||||
else raise "Unknown access #{params['access']}"
|
||||
end
|
||||
end
|
||||
|
||||
def get_group_from_loaded_parameters
|
||||
get_group(@database_name, @name)
|
||||
end
|
||||
|
||||
def get_group(database_name, name)
|
||||
Group.where(organization_id: Organization.find_by_database_name(database_name).id, name: name).first
|
||||
end
|
||||
|
||||
def load_mandatory_group
|
||||
@group = get_group_from_loaded_parameters
|
||||
render json: { errors: "Group with database_name #{@database_name} and name #{@name} not found" }, status: 404 unless @group
|
||||
end
|
||||
|
||||
def load_user_from_username
|
||||
@user = Carto::User.where(username: @username).first
|
||||
end
|
||||
|
||||
def load_users_from_username
|
||||
@users = @usernames.map { |username| Carto::User.where(username: username).first }
|
||||
end
|
||||
|
||||
def load_table
|
||||
@table = Carto::Visualization.where(user_id: @user.id, type: 'table', name: @table_name).first
|
||||
render json: { errors: "Table #{@username}.#{@table_name} not found" }, status: 404 unless @table
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class ExternalSourcePresenter
|
||||
|
||||
def initialize(external_source)
|
||||
@external_source = external_source
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return {} if @external_source.nil?
|
||||
|
||||
{
|
||||
size: @external_source.size,
|
||||
row_count: @external_source.rows_counted,
|
||||
geometry_types: @external_source.geometry_types
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,105 @@
|
||||
module Carto
|
||||
module Api
|
||||
class GeocodingsController < ::Api::ApplicationController
|
||||
GEOCODING_SQLAPI_CALLS_TIMEOUT = 45
|
||||
|
||||
ssl_required :index, :show, :available_geometries, :estimation_for
|
||||
|
||||
def index
|
||||
# data_import_id is set for content guessing geocodes
|
||||
# TODO: agree on a more flexible API with params
|
||||
geocodings = Carto::Geocoding.where(
|
||||
"user_id = ? AND (state NOT IN (?)) AND (data_import_id IS NULL)",
|
||||
current_user.id, ['failed', 'finished', 'cancelled']
|
||||
).all
|
||||
render json: { geocodings: geocodings }, root: false
|
||||
end
|
||||
|
||||
def show
|
||||
geocoding = Carto::Geocoding.where(user_id: current_user.id, id: params[:id]).first
|
||||
raise RecordNotFound unless geocoding
|
||||
render json: geocoding.public_values
|
||||
end
|
||||
|
||||
def available_geometries
|
||||
case params[:kind]
|
||||
when 'admin1'
|
||||
return render(json: ['polygon'])
|
||||
when 'namedplace'
|
||||
return render(json: ['point'])
|
||||
when 'postalcode'
|
||||
return available_geometries_for_postalcode
|
||||
else
|
||||
return head(400)
|
||||
end
|
||||
end
|
||||
|
||||
def estimation_for
|
||||
table = get_table(params[:table_name])
|
||||
render_jsonp( { description: "table #{params[:table_name]} doesn't exist" }, 500) and return unless table
|
||||
|
||||
force_all_rows = (params[:force_all_rows].to_s == 'true')
|
||||
total_rows = Carto::Geocoding.processable_rows(table, force_all_rows)
|
||||
remaining_quota = uri_user.remaining_geocoding_quota
|
||||
remaining_quota = (remaining_quota > 0 ? remaining_quota : 0)
|
||||
used_credits = total_rows - remaining_quota
|
||||
used_credits = (used_credits > 0 ? used_credits : 0)
|
||||
render json: {
|
||||
rows: total_rows,
|
||||
estimation: (uri_user.geocoding_block_price.to_i * used_credits) / Carto::User::GEOCODING_BLOCK_SIZE.to_f
|
||||
}
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, params: params)
|
||||
render_jsonp( { description: e.message }, 500)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: this should be moved upwards in the controller hierarchy, and make it a replacement for current_user
|
||||
def uri_user
|
||||
@uri_user ||= Carto::User.where(id: current_user.id).first
|
||||
end
|
||||
|
||||
def available_geometries_for_postalcode
|
||||
return head(400) unless params[:free_text] || (params[:column_name] && params[:table_name])
|
||||
|
||||
input = params[:free_text].present? ? clean_free_text_input([params[:free_text]]) : select_distinct_from_table_and_column(params[:table_name], params[:column_name])
|
||||
return head(400) if input.nil? && params[:table_name].present?
|
||||
render(json: []) and return if input.nil? || input.empty?
|
||||
|
||||
list = input.map { |v| "'#{v.to_s.gsub("'", "''")}'" }.join(",")
|
||||
|
||||
internal_geocoder_api_config = CartoDB::GeocoderConfig.instance.get['internal']
|
||||
.symbolize_keys
|
||||
.merge(timeout: GEOCODING_SQLAPI_CALLS_TIMEOUT)
|
||||
services = CartoDB::SQLApi.new(internal_geocoder_api_config)
|
||||
.fetch("SELECT (admin0_available_services(Array[#{list}])).*")
|
||||
|
||||
geometries = []
|
||||
points = services.select { |s| s['postal_code_points'] }.size
|
||||
polygons = services.select { |s| s['postal_code_polygons'] }.size
|
||||
geometries.append 'point' if points > 0 && points >= polygons
|
||||
geometries.append 'polygon' if polygons > 0 && polygons >= points
|
||||
|
||||
render(json: geometries)
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, params: params)
|
||||
end
|
||||
|
||||
def clean_free_text_input(input)
|
||||
input.map{ |v| v.to_s.squish.downcase.gsub(/[^\p{Alnum}]/, '') }.reject(&:blank?)
|
||||
end
|
||||
|
||||
def select_distinct_from_table_and_column(table_name, column_name)
|
||||
table = get_table(table_name)
|
||||
return nil if table.nil?
|
||||
input = table.sequel.distinct.select_map(params[:column_name].to_sym)
|
||||
end
|
||||
|
||||
def get_table(table_name)
|
||||
Carto::Helpers::TableLocator.new.get_by_id_or_name(table_name, uri_user).try(&:service)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
module Carto
|
||||
module Api
|
||||
class GrantablePresenter
|
||||
|
||||
def initialize(grantable)
|
||||
@grantable = grantable
|
||||
end
|
||||
|
||||
def to_poro
|
||||
{
|
||||
id: @grantable.id,
|
||||
type: @grantable.type,
|
||||
name: @grantable.name,
|
||||
avatar_url: @grantable.avatar_url,
|
||||
model: model_presenter.to_poro
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def model_presenter
|
||||
case @grantable.type
|
||||
when 'user'
|
||||
Carto::Api::UserPresenter.new(Carto::User.find(@grantable.id), fetch_groups: true)
|
||||
when 'group'
|
||||
Carto::Api::GroupPresenter.new(Carto::Group.find(@grantable.id), fetch_users: true)
|
||||
else
|
||||
raise "Unknown grantable type #{@grantable.type}"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
require_dependency 'cartodb/errors'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
|
||||
class GrantablesController < ::Api::ApplicationController
|
||||
include PagedSearcher
|
||||
include Carto::ControllerHelper
|
||||
|
||||
respond_to :json
|
||||
|
||||
ssl_required :index
|
||||
|
||||
before_filter :load_organization
|
||||
|
||||
VALID_ORDER_PARAMS = [:id, :name, :type, :avatar_url, :organization_id, :updated_at].freeze
|
||||
|
||||
def index
|
||||
page, per_page, order, _order_direction = page_per_page_order_params(VALID_ORDER_PARAMS)
|
||||
query = params[:q]
|
||||
|
||||
grantable_query = Carto::GrantableQueryBuilder.new(@organization).with_filter(query)
|
||||
grantables = grantable_query.run(page, per_page, order)
|
||||
total_entries = grantable_query.count
|
||||
|
||||
render_jsonp({
|
||||
grantables: grantables.map { |g| Carto::Api::GrantablePresenter.new(g).to_poro },
|
||||
total_entries: total_entries
|
||||
}, 200)
|
||||
rescue Carto::ParamInvalidError => e
|
||||
render json: { errors: e.message }, status: e.status
|
||||
rescue StandardError => e
|
||||
CartoDB.notify_exception(e, { params: params })
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_organization
|
||||
@organization = Carto::Organization.where(id: params['organization_id']).first
|
||||
render json: { errors: "Organization #{params['organization_id']} not found" }, status: 404 unless @organization
|
||||
render json: { errors: "You don't belong to organization #{params['organization_id']}" }, status: 400 unless current_user.organization_id == @organization.id
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
require_relative 'user_presenter'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class GroupPresenter
|
||||
|
||||
FULL_FETCH_OPTIONS = {
|
||||
fetch_shared_tables_count: true,
|
||||
fetch_shared_maps_count: true,
|
||||
fetch_users: true
|
||||
}
|
||||
|
||||
def self.full(group)
|
||||
Carto::Api::GroupPresenter.new(group, FULL_FETCH_OPTIONS)
|
||||
end
|
||||
|
||||
# Available fetching options:
|
||||
# - fetch_shared_tables_count
|
||||
# - fetch_shared_maps_count
|
||||
# - fetch_users
|
||||
def initialize(group, fetching_options = {})
|
||||
@group = group
|
||||
@fetching_options = fetching_options
|
||||
end
|
||||
|
||||
def to_poro
|
||||
poro = {
|
||||
id: @group.id,
|
||||
organization_id: @group.organization_id,
|
||||
name: @group.name,
|
||||
display_name: @group.display_name
|
||||
}
|
||||
|
||||
if @fetching_options[:fetch_shared_tables_count] == true
|
||||
poro.merge!({ shared_tables_count: shared_tables_count })
|
||||
end
|
||||
if @fetching_options[:fetch_shared_maps_count] == true
|
||||
poro.merge!({ shared_maps_count: shared_maps_count })
|
||||
end
|
||||
if @fetching_options[:fetch_users] == true
|
||||
poro.merge!({ users: users })
|
||||
end
|
||||
|
||||
poro
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def shared_tables_count
|
||||
shared_visualizations_query.where(:visualizations => { type: Carto::Visualization::TYPE_CANONICAL }).count
|
||||
end
|
||||
|
||||
def shared_maps_count
|
||||
shared_visualizations_query.where(:visualizations => { type: Carto::Visualization::TYPE_DERIVED }).count
|
||||
end
|
||||
|
||||
def shared_visualizations_query
|
||||
Carto::SharedEntity.where(recipient_id: @group.id).joins(:visualization)
|
||||
end
|
||||
|
||||
def users
|
||||
@group.users.map { |u| Carto::Api::UserPresenter.new(u, { fetch_groups: false } ).to_poro }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,178 @@
|
||||
require_relative 'paged_searcher'
|
||||
require_dependency 'cartodb/errors'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
|
||||
class GroupsController < ::Api::ApplicationController
|
||||
include PagedSearcher
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :index, :show, :create, :update, :destroy, :add_users, :remove_users
|
||||
|
||||
before_filter :load_fetching_options, only: [:show, :index]
|
||||
before_filter :load_organization
|
||||
before_filter :load_user
|
||||
before_filter :validate_organization_or_user_loaded
|
||||
before_filter :load_group, only: [:show, :update, :destroy, :add_users, :remove_users]
|
||||
before_filter :org_admin_only, only: [:create, :update, :destroy, :add_users, :remove_users]
|
||||
before_filter :org_users_only, only: [:show, :index]
|
||||
before_filter :load_organization_users, only: [:add_users, :remove_users]
|
||||
before_filter :valid_password_confirmation, only: [:destroy, :add_users, :remove_users]
|
||||
|
||||
rescue_from Carto::ParamInvalidError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::PasswordConfirmationError, with: :rescue_from_password_confirmation_error
|
||||
|
||||
VALID_ORDER_PARAMS = [:id, :name, :display_name, :organization_id, :updated_at].freeze
|
||||
|
||||
def index
|
||||
page, per_page, order, _order_direction = page_per_page_order_params(VALID_ORDER_PARAMS)
|
||||
|
||||
groups = @user ? @user.groups : @organization.groups
|
||||
groups = groups.where('name ilike ?', "%#{params[:q]}%") if params[:q]
|
||||
total_entries = groups.count
|
||||
|
||||
groups = Carto::PagedModel.paged_association(groups, page, per_page, order)
|
||||
|
||||
render_jsonp({
|
||||
groups: groups.map { |g| Carto::Api::GroupPresenter.new(g, @fetching_options).to_poro },
|
||||
total_entries: total_entries
|
||||
}, 200)
|
||||
end
|
||||
|
||||
def show
|
||||
render_jsonp(Carto::Api::GroupPresenter.new(@group, @fetching_options).to_poro, 200)
|
||||
end
|
||||
|
||||
def create
|
||||
group = @organization.create_group(params['display_name'])
|
||||
render_jsonp(Carto::Api::GroupPresenter.full(group).to_poro, 200)
|
||||
rescue CartoDB::ModelAlreadyExistsError => e
|
||||
CartoDB::Logger.debug(message: 'Group already exists', exception: e, params: params)
|
||||
render json: { errors: ["A group with that name already exists"] }, status: 409
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
handle_statement_invalid_error(e, group)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e, params: params, group: group || 'no group', organization: @organization)
|
||||
render json: { errors: [e.message] }, status: 500
|
||||
end
|
||||
|
||||
def update
|
||||
@group.rename_group_with_extension(params['display_name'])
|
||||
render_jsonp(Carto::Api::GroupPresenter.full(@group).to_poro, 200)
|
||||
rescue CartoDB::ModelAlreadyExistsError => e
|
||||
CartoDB::Logger.debug(message: 'Group display name already exists', params: params)
|
||||
render json: { errors: ["A group with that name already exists"] }, status: 409
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
handle_statement_invalid_error(e, @group)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e, params: params, group: @group)
|
||||
render json: { errors: [e.message] }, status: 500
|
||||
end
|
||||
|
||||
def destroy
|
||||
@group.destroy_group_with_extension
|
||||
render json: {}, status: 204
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
handle_statement_invalid_error(e, @group)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e, params: params, group: @group)
|
||||
render json: { errors: [e.message] }, status: 500
|
||||
end
|
||||
|
||||
def add_users
|
||||
@group.add_users_with_extension(@organization_users)
|
||||
render json: {}, status: 200
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
handle_statement_invalid_error(e, @group)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e, user: @user, params: params, group: @group)
|
||||
render json: { errors: [e.message] }, status: 500
|
||||
end
|
||||
|
||||
def remove_users
|
||||
@group.remove_users_with_extension(@organization_users)
|
||||
render json: {}, status: 200
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
handle_statement_invalid_error(e, @group)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e, user: @user, params: params, group: @group)
|
||||
render json: { errors: [e.message] }, status: 500
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_fetching_options
|
||||
@fetching_options = {
|
||||
fetch_shared_tables_count: params[:fetch_shared_tables_count] == 'true',
|
||||
fetch_shared_maps_count: params[:fetch_shared_maps_count] == 'true',
|
||||
fetch_users: params[:fetch_users] == 'true'
|
||||
}
|
||||
end
|
||||
|
||||
def load_organization
|
||||
return unless params['organization_id'].present?
|
||||
@organization = Carto::Organization.where(id: params['organization_id']).first
|
||||
render json: { errors: ["Org. #{params['organization_id']} not found"] }, status: 404 unless @organization
|
||||
end
|
||||
|
||||
def load_user
|
||||
return unless params['user_id'].present?
|
||||
|
||||
@user = Carto::User.where(id: params['user_id']).first
|
||||
render json: { errors: ["User #{params['user_id']} not found"] }, status: 404 unless @user
|
||||
|
||||
if @organization.nil?
|
||||
@organization = @user.organization
|
||||
elsif @user.organization_id != @organization.id
|
||||
render json: { errors: ["You can't get other organization users"] }, status: 501
|
||||
end
|
||||
|
||||
unless @user.id == current_user.id || current_user.organization_admin?
|
||||
render json: { errors: ["You can't get other users groups"] }, status: 501
|
||||
end
|
||||
end
|
||||
|
||||
def validate_organization_or_user_loaded
|
||||
render json: { errors: ["You must set user_id or organization_id"] }, status: 404 unless @organization || @user
|
||||
end
|
||||
|
||||
def org_users_only
|
||||
unless @organization.id == current_user.organization_id
|
||||
render json: { errors: ["Not organization user"] }, status: 400
|
||||
end
|
||||
end
|
||||
|
||||
def org_admin_only
|
||||
unless @organization.admin?(current_user)
|
||||
render json: { errors: ["Not org. admin"] }, status: 400
|
||||
end
|
||||
end
|
||||
|
||||
def load_group
|
||||
@group = @organization.groups.where(id: params['group_id']).first
|
||||
render json: { errors: ["Group #{params['group_id']} not found"] }, status: 404 unless @group
|
||||
end
|
||||
|
||||
def load_organization_users
|
||||
ids = params['users'].present? ? params['users'] : [ params['user_id'] ]
|
||||
@organization_users = ids.map { |id| @organization.users.where(id: id).first }
|
||||
render json: { errors: ["Users #{ids} not found"] }, status: 404 if @organization_users.empty?
|
||||
end
|
||||
|
||||
def handle_statement_invalid_error(e, group)
|
||||
err_regexp = /ERROR: (.*)\n/
|
||||
if e.message =~ err_regexp
|
||||
render json: { errors: [err_regexp.match(e.message)[1]] }, status: 422
|
||||
else
|
||||
CartoDB::Logger.error(exception: e, params: params, group: group || 'no group', organization: @organization)
|
||||
render json: { errors: [e.message] }, status: 500
|
||||
end
|
||||
end
|
||||
|
||||
def rescue_from_password_confirmation_error(error)
|
||||
render_jsonp({ message: "Error modifying groups", errors: [error.message] }, 403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class ImageProxyController < ::Api::ApplicationController
|
||||
|
||||
ssl_required :show
|
||||
|
||||
def show
|
||||
# image url
|
||||
url = params[:url]
|
||||
render :status => 404 if url.blank?
|
||||
|
||||
# fetch the image
|
||||
http_client = Carto::Http::Client.get('image_proxy', log_requests: true)
|
||||
response = http_client.get(url, followlocation: true, timeout: 3)
|
||||
if response.code == 200
|
||||
send_data response.response_body, type: response.headers['Content-Type'], disposition: 'inline'
|
||||
else
|
||||
render :status => 404
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
module Carto
|
||||
module Api
|
||||
class ImportsController < ::Api::ApplicationController
|
||||
|
||||
ssl_required :index, :show
|
||||
ssl_allowed :service_token_valid?, :list_files_for_service, :get_service_auth_url, :validate_service_oauth_code,
|
||||
:service_oauth_callback
|
||||
|
||||
def index
|
||||
imports = DataImportsService.new.process_recent_user_imports(current_user)
|
||||
render json: { imports: imports.map(&:id), success: true }
|
||||
end
|
||||
|
||||
def show
|
||||
import = DataImportsService.new.process_by_id(params[:id])
|
||||
render_404 and return if import.nil?
|
||||
|
||||
data = Carto::Api::DataImportPresenter.new(import).api_public_values
|
||||
if import.state == Carto::DataImport::STATE_COMPLETE
|
||||
data[:any_table_raster] = import.is_raster?
|
||||
|
||||
decorate_twitter_import_data!(data, import)
|
||||
decorate_default_visualization_data!(data, import)
|
||||
end
|
||||
|
||||
render json: data
|
||||
end
|
||||
|
||||
def service_token_valid?
|
||||
valid = DataImportsService.new.validate_synchronization_oauth(uri_user, params[:id])
|
||||
render_jsonp({ oauth_valid: valid, success: true })
|
||||
rescue CartoDB::Datasources::TokenExpiredOrInvalidError => e
|
||||
CartoDB.notify_exception(e, { user: uri_user, params: params })
|
||||
render_jsonp({ errors: e.message }, 401)
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { user: uri_user, params: params })
|
||||
render_jsonp({ errors: e.message }, 400)
|
||||
end
|
||||
|
||||
def list_files_for_service
|
||||
filter = params[:filter].present? ? params[:filter] : []
|
||||
results = DataImportsService.new.get_service_files(uri_user, params[:id], filter)
|
||||
render_jsonp({ files: results, success: true })
|
||||
rescue CartoDB::Datasources::TokenExpiredOrInvalidError => e
|
||||
CartoDB.notify_exception(e, { user: uri_user, params: params })
|
||||
render_jsonp({ errors: e.message }, 401)
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { user: uri_user, params: params })
|
||||
render_jsonp({ errors: { imports: e.message } }, 400)
|
||||
end
|
||||
|
||||
def get_service_auth_url
|
||||
auth_url = DataImportsService.new.get_service_auth_url(uri_user, params[:id])
|
||||
render_jsonp({ url: auth_url, success: true})
|
||||
rescue CartoDB::Datasources::TokenExpiredOrInvalidError => e
|
||||
CartoDB.notify_exception(e, { user: uri_user, params: params })
|
||||
render_jsonp({ errors: e.message }, 401)
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { user: uri_user, params: params })
|
||||
render_jsonp({ errors: { imports: e.message } }, 400)
|
||||
end
|
||||
|
||||
def validate_service_oauth_code
|
||||
success = DataImportsService.new.validate_service_oauth_code(uri_user, params[:id], params[:code])
|
||||
render_jsonp({ success: success })
|
||||
rescue CartoDB::Datasources::TokenExpiredOrInvalidError => e
|
||||
CartoDB.notify_exception(e, { user: uri_user, params: params })
|
||||
render_jsonp({ errors: e.message }, 401)
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, { user: uri_user, params: params })
|
||||
render_jsonp({ errors: { imports: e.message } }, 400)
|
||||
end
|
||||
|
||||
def service_oauth_callback
|
||||
DataImportsService.new.validate_callback(uri_user, params[:id], params)
|
||||
request.format = 'html'
|
||||
respond_to do |format|
|
||||
format.all { render text: '<script>window.close();</script>', content_type: 'text/html' }
|
||||
end
|
||||
rescue CartoDB::Datasources::TokenExpiredOrInvalidError => e
|
||||
CartoDB::Logger.warning(message: "Expired oauth token", exception: e, user: uri_user, params: params)
|
||||
render text: 'Expired token. Try reconnecting<script>setTimeout(function(){window.close()}, 1000);</script>',
|
||||
content_type: 'text/html', status: 401
|
||||
rescue => e
|
||||
CartoDB::Logger.warning(message: "Error in oauth callback", exception: e, user: uri_user, params: params)
|
||||
render text: 'Connection failed<script>setTimeout(function(){window.close()}, 1000);</script>',
|
||||
content_type: 'text/html', status: 400
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: this should be moved upwards in the controller hierarchy, and make it a replacement for current_user
|
||||
def uri_user
|
||||
@uri_user ||= Carto::User.where(id: current_user.id).first
|
||||
end
|
||||
|
||||
def decorate_twitter_import_data!(data, data_import)
|
||||
return if data_import.service_name != CartoDB::Datasources::Search::Twitter::DATASOURCE_NAME
|
||||
|
||||
audit_entry = ::SearchTweet.where(data_import_id: data_import.id).first
|
||||
data[:tweets_georeferenced] = audit_entry.retrieved_items
|
||||
data[:tweets_cost] = audit_entry.price
|
||||
data[:tweets_overquota] = audit_entry.user.remaining_twitter_quota == 0
|
||||
end
|
||||
|
||||
def decorate_default_visualization_data!(data, data_import)
|
||||
derived_vis_id = nil
|
||||
|
||||
if data_import.create_visualization && !data_import.visualization_id.nil?
|
||||
derived_vis_id = data_import.visualization_id
|
||||
end
|
||||
|
||||
data[:derived_visualization_id] = derived_vis_id
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
module Carto
|
||||
module Api
|
||||
module InfowindowMigrator
|
||||
MUSTACHE_ROOT_PATH = 'lib/assets/javascripts/builder/mustache-templates'.freeze
|
||||
|
||||
def migrate_builder_infowindow(templated_element, mustache_dir: 'infowindows')
|
||||
return nil if templated_element.nil?
|
||||
|
||||
template = templated_element['template']
|
||||
return templated_element if template.present?
|
||||
|
||||
templated_sym = templated_element.deep_symbolize_keys
|
||||
|
||||
old_template_name = templated_sym[:template_name]
|
||||
return templated_element if old_template_name == 'none'
|
||||
|
||||
fields = templated_element['fields']
|
||||
unless fields.present?
|
||||
templated_element['template_name'] = 'none'
|
||||
return templated_element
|
||||
end
|
||||
|
||||
if MIGRATED_TEMPLATES.include?(old_template_name)
|
||||
new_template_name = 'infowindow_color'
|
||||
|
||||
fixed_color = extract_color_from_old_template(old_template_name)
|
||||
|
||||
template_content_path = "#{MUSTACHE_ROOT_PATH}/#{mustache_dir}/infowindow_color.jst.mustache"
|
||||
|
||||
templated_element[:template] = get_template(
|
||||
new_template_name,
|
||||
templated_sym[:template],
|
||||
template_content_path).gsub('#35AAE5', fixed_color)
|
||||
|
||||
templated_element[:headerColor] = {
|
||||
color: {
|
||||
opacity: 1,
|
||||
fixed: fixed_color
|
||||
}
|
||||
}
|
||||
else
|
||||
new_template_name = parse_old_template_name(old_template_name)
|
||||
|
||||
templated_element[:template] = get_template(
|
||||
old_template_name,
|
||||
templated_sym[:template],
|
||||
"#{MUSTACHE_ROOT_PATH}/#{mustache_dir}/#{get_template_name(old_template_name)}.jst.mustache")
|
||||
end
|
||||
|
||||
templated_element[:template_name] = new_template_name
|
||||
|
||||
templated_element
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
INFOWINDOW_COLOR_TEMPLATE = 'infowindow_color'.freeze
|
||||
|
||||
MIGRATED_TEMPLATES = %w{ infowindow_light_header_blue infowindow_light_header_yellow
|
||||
infowindow_light_header_orange infowindow_light_header_green }.freeze
|
||||
|
||||
COLOR_MAP = {
|
||||
'blue' => '#35AAE5',
|
||||
'green' => '#7FC97F',
|
||||
'orange' => '#E68165',
|
||||
'yellow' => '#E5C13D'
|
||||
}.freeze
|
||||
|
||||
def extract_color_from_old_template(old_template_name)
|
||||
COLOR_MAP[old_template_name.split('_').last]
|
||||
end
|
||||
|
||||
def parse_old_template_name(old_template_name)
|
||||
Pathname.new(old_template_name).basename.to_s
|
||||
rescue => exception
|
||||
CartoDB::Logger.error(message: "#{self.class}: Error parsing template",
|
||||
exception: exception,
|
||||
parsing: old_template_name)
|
||||
|
||||
old_template_name
|
||||
end
|
||||
|
||||
def get_template_name(name)
|
||||
Carto::Layer::TEMPLATES_MAP.fetch(name, name)
|
||||
end
|
||||
|
||||
def get_template(template_name, fallback_template, template_path)
|
||||
if template_name.present?
|
||||
path = Rails.root.join(template_path)
|
||||
File.read(path)
|
||||
else
|
||||
fallback_template
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
module Carto
|
||||
module Api
|
||||
class InvitationPresenter
|
||||
|
||||
def initialize(invitation)
|
||||
@invitation = invitation
|
||||
end
|
||||
|
||||
def to_poro
|
||||
{
|
||||
id: @invitation.id,
|
||||
users_emails: @invitation.users_emails,
|
||||
welcome_text: @invitation.welcome_text,
|
||||
viewer: @invitation.viewer,
|
||||
created_at: @invitation.created_at,
|
||||
updated_at: @invitation.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
module Carto
|
||||
module Api
|
||||
class InvitationsController < ::Api::ApplicationController
|
||||
|
||||
ssl_required :create
|
||||
|
||||
before_filter :load_organization
|
||||
|
||||
def create
|
||||
@organization.update_attribute(:auth_username_password_enabled, true) if params[:enable_organization_signup] === true
|
||||
|
||||
invitation = Carto::Invitation.create_new(
|
||||
Carto::User.find(current_user.id),
|
||||
params[:users_emails],
|
||||
params[:welcome_text],
|
||||
params[:viewer]
|
||||
)
|
||||
if invitation.valid?
|
||||
render_jsonp(Carto::Api::InvitationPresenter.new(invitation).to_poro)
|
||||
else
|
||||
render json: { errors: invitation.errors }, status: 400
|
||||
end
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, params: params , invitation: (invitation ? invitation : 'not created'))
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_organization
|
||||
@organization = Carto::Organization.where(id: params[:organization_id]).first
|
||||
render_404 && return unless @organization
|
||||
unless @organization.admin?(current_user)
|
||||
render_jsonp({ errors: { organization: 'not admin' } }, 401) && return
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,839 @@
|
||||
require_dependency 'carto/api/infowindow_migrator'
|
||||
require_dependency 'carto/table_utils'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class LayerPresenter
|
||||
include InfowindowMigrator
|
||||
include Carto::TableUtils
|
||||
|
||||
PUBLIC_VALUES = %W{ options kind infowindow tooltip id order }
|
||||
|
||||
# CSS is not stored by default, only when sent by frontend,
|
||||
# so this is returned whenever a layer that needs CSS but has none is requestesd
|
||||
EMPTY_CSS = '#dummy{}'
|
||||
|
||||
TORQUE_ATTRS = %w(
|
||||
table_name
|
||||
user_name
|
||||
property
|
||||
blendmode
|
||||
resolution
|
||||
countby
|
||||
torque-duration
|
||||
torque-steps
|
||||
torque-blend-mode
|
||||
query
|
||||
tile_style
|
||||
named_map
|
||||
visible
|
||||
)
|
||||
|
||||
INFOWINDOW_KEYS = %w(
|
||||
fields template_name template alternative_names width maxHeight
|
||||
)
|
||||
|
||||
# Options:
|
||||
# - viewer_user
|
||||
# - user: owner user
|
||||
# - with_style_properties: request style_properties generation if needed or not. Default: false.
|
||||
def initialize(layer, options = {}, configuration = {}, decoration_data = {})
|
||||
@layer = layer
|
||||
@options = options
|
||||
@configuration = configuration
|
||||
@decoration_data = decoration_data
|
||||
|
||||
@viewer_user = options.fetch(:viewer_user, nil)
|
||||
@owner_user = options.fetch(:user, nil)
|
||||
@with_style_properties = options.fetch(:with_style_properties, false)
|
||||
end
|
||||
|
||||
def to_poro(migrate_builder_infowindows: false)
|
||||
poro = base_poro(@layer)
|
||||
if migrate_builder_infowindows
|
||||
if poro['infowindow'].present?
|
||||
poro['infowindow'] = migrate_builder_infowindow(poro['infowindow'])
|
||||
end
|
||||
if poro['tooltip'].present?
|
||||
poro['tooltip'] = migrate_builder_infowindow(poro['tooltip'], mustache_dir: 'tooltips')
|
||||
end
|
||||
end
|
||||
poro
|
||||
end
|
||||
|
||||
def to_json
|
||||
public_values(@layer).to_json
|
||||
end
|
||||
|
||||
def to_embed_poro
|
||||
{
|
||||
id: @layer.id,
|
||||
options: @layer.options.select { |k| ['tile_style', 'style_version'].include?(k) }
|
||||
}
|
||||
end
|
||||
|
||||
def to_vizjson_v2
|
||||
if base?(@layer)
|
||||
with_kind_as_type(base_poro(@layer)).symbolize_keys
|
||||
elsif torque?(@layer)
|
||||
as_torque
|
||||
else
|
||||
{
|
||||
id: @layer.id,
|
||||
type: 'CartoDB',
|
||||
infowindow: infowindow_data_v2,
|
||||
tooltip: tooltip_data_v2,
|
||||
legend: @layer.legend,
|
||||
order: @layer.order,
|
||||
visible: public_values(@layer).symbolize_keys[:options]['visible'],
|
||||
options: options_data_v2
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def to_vizjson_v1
|
||||
return base_poro(@layer).symbolize_keys if base?(@layer)
|
||||
{
|
||||
id: @layer.id,
|
||||
kind: 'CartoDB',
|
||||
infowindow: infowindow_data_v1,
|
||||
order: @layer.order,
|
||||
options: options_data_v1
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def viewer_is_owner?
|
||||
return (@owner_user.id == @viewer_user.id) if (@owner_user && @viewer_user)
|
||||
|
||||
# This can be removed if 'user_name' support is dropped
|
||||
layer_opts = @layer.options.nil? ? Hash.new : @layer.options
|
||||
if @viewer_user && layer_opts['user_name'] && layer_opts['table_name']
|
||||
@viewer_user.username == layer_opts['user_name']
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
# INFO: Assumes table_name needs to always be qualified, don't call if doesn't
|
||||
def qualify_table_name
|
||||
layer_opts = @layer.options.nil? ? Hash.new : @layer.options
|
||||
|
||||
# if the table_name already have a schema don't add another one.
|
||||
# This case happens when you share a layer already shared with you
|
||||
return layer_opts['table_name'] if layer_opts['table_name'].include?('.')
|
||||
|
||||
if @owner_user && @viewer_user
|
||||
@layer.qualified_table_name(@owner_user)
|
||||
else
|
||||
# TODO: Legacy support: Remove 'user_name' and use always :viewer_user and :user
|
||||
user_name = layer_opts['user_name']
|
||||
if user_name.include?('-')
|
||||
"\"#{layer_opts['user_name']}\".#{safe_table_name_quoting(layer_opts['table_name'])}"
|
||||
else
|
||||
"#{layer_opts['user_name']}.#{safe_table_name_quoting(layer_opts['table_name'])}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def base_poro(layer)
|
||||
# .merge left for backwards compatibility
|
||||
public_values(layer).merge('options' => layer_options)
|
||||
end
|
||||
|
||||
def public_values(layer)
|
||||
Hash[ PUBLIC_VALUES.map { |attribute| [attribute, layer.send(attribute)] } ]
|
||||
end
|
||||
|
||||
# Decorates the layer presentation with data if needed. nils on the decoration act as removing the field
|
||||
def decorate_with_data(source_hash, decoration_data)
|
||||
decoration_data.each { |key, value|
|
||||
source_hash[key] = value
|
||||
source_hash.delete_if { |k, v|
|
||||
v.nil?
|
||||
}
|
||||
}
|
||||
source_hash
|
||||
end
|
||||
|
||||
def base?(layer)
|
||||
['tiled', 'background', 'gmapsbase', 'wms'].include? layer.kind
|
||||
end
|
||||
|
||||
def torque?(layer)
|
||||
layer.kind == 'torque'
|
||||
end
|
||||
|
||||
def with_template(infowindow, path)
|
||||
# Careful with this logic:
|
||||
# - nil means absolutely no infowindow (e.g. a torque)
|
||||
# - path = nil or template filled: either pre-filled or custom infowindow, nothing to do here
|
||||
# - template and path not nil but template not filled: stay and fill
|
||||
return nil if infowindow.nil?
|
||||
|
||||
template = infowindow['template']
|
||||
return infowindow if (!template.nil? && !template.empty?) || path.nil?
|
||||
|
||||
infowindow[:template] = File.read(path)
|
||||
infowindow
|
||||
end
|
||||
|
||||
def layer_options
|
||||
layer_opts = @layer.options.nil? ? Hash.new : @layer.options
|
||||
if layer_opts['table_name'] && !viewer_is_owner?
|
||||
layer_opts['table_name'] = qualify_table_name
|
||||
end
|
||||
|
||||
if @with_style_properties &&
|
||||
(layer_opts['style_properties'].nil? || layer_opts['style_properties']['autogenerated'] == true)
|
||||
StylePropertiesGenerator.new(@layer).migrate(layer_opts)
|
||||
end
|
||||
|
||||
layer_opts
|
||||
end
|
||||
|
||||
def options_data_v1
|
||||
return @layer.options if @options[:full]
|
||||
@layer.options.select { |key, value| public_options.include?(key.to_s) }
|
||||
end
|
||||
|
||||
def options_data_v2
|
||||
if @options[:full]
|
||||
decorate_with_data(@layer.options, @decoration_data)
|
||||
else
|
||||
sql = sql_from(@layer.options)
|
||||
data = {
|
||||
sql: wrap(sql, @layer.options),
|
||||
layer_name: name_for(@layer),
|
||||
cartocss: css_from(@layer.options),
|
||||
cartocss_version: @layer.options.fetch('style_version'), # Mandatory
|
||||
interactivity: @layer.options.fetch('interactivity') # Mandatory
|
||||
}
|
||||
data = decorate_with_data(data, @decoration_data)
|
||||
|
||||
if @viewer_user
|
||||
if @layer.options['table_name'] && !viewer_is_owner?
|
||||
data['table_name'] = qualify_table_name
|
||||
end
|
||||
end
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
def with_kind_as_type(attributes)
|
||||
decorate_with_data(attributes.merge(type: attributes.delete('kind')), @decoration_data)
|
||||
end
|
||||
|
||||
def as_torque
|
||||
api_templates_type = @options.fetch(:https_request, false) ? 'private' : 'public'
|
||||
layer_options = decorate_with_data(
|
||||
# Make torque always have a SQL query too (as vizjson v2)
|
||||
@layer.options.merge({ 'query' => wrap(sql_from(@layer.options), @layer.options) }),
|
||||
@decoration_data
|
||||
)
|
||||
|
||||
{
|
||||
id: @layer.id,
|
||||
type: 'torque',
|
||||
order: @layer.order,
|
||||
legend: @layer.legend,
|
||||
options: {
|
||||
stat_tag: @options.fetch(:visualization_id),
|
||||
maps_api_template: ApplicationHelper.maps_api_template(api_templates_type),
|
||||
sql_api_template: ApplicationHelper.sql_api_template(api_templates_type),
|
||||
# tiler_* is kept for backwards compatibility
|
||||
tiler_protocol: (@configuration[:tiler]["public"]["protocol"] rescue nil),
|
||||
tiler_domain: (@configuration[:tiler]["public"]["domain"] rescue nil),
|
||||
tiler_port: (@configuration[:tiler]["public"]["port"] rescue nil),
|
||||
# sql_api_* is kept for backwards compatibility
|
||||
sql_api_protocol: (@configuration[:sql_api]["public"]["protocol"] rescue nil),
|
||||
sql_api_domain: (@configuration[:sql_api]["public"]["domain"] rescue nil),
|
||||
sql_api_endpoint: (@configuration[:sql_api]["public"]["endpoint"] rescue nil),
|
||||
sql_api_port: (@configuration[:sql_api]["public"]["port"] rescue nil),
|
||||
layer_name: name_for(@layer),
|
||||
}.merge(
|
||||
layer_options.select { |k| TORQUE_ATTRS.include? k })
|
||||
}
|
||||
end
|
||||
|
||||
def infowindow_data_v1
|
||||
with_template(@layer.infowindow, @layer.infowindow_template_path)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e)
|
||||
throw e
|
||||
end
|
||||
|
||||
def infowindow_data_v2
|
||||
whitelisted_infowindow(with_template(@layer.infowindow, @layer.infowindow_template_path))
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e)
|
||||
throw e
|
||||
end
|
||||
|
||||
def tooltip_data_v2
|
||||
whitelisted_infowindow(with_template(@layer.tooltip, @layer.tooltip_template_path))
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e)
|
||||
throw e
|
||||
end
|
||||
|
||||
def name_for(layer)
|
||||
layer_alias = layer.options.fetch('table_name_alias', nil)
|
||||
table_name = layer.options['table_name']
|
||||
|
||||
return table_name unless layer_alias && !layer_alias.empty?
|
||||
layer_alias
|
||||
end
|
||||
|
||||
def sql_from(options)
|
||||
query = options.fetch('query', '')
|
||||
return default_query_for(options) if query.nil? || query.empty?
|
||||
query
|
||||
end
|
||||
|
||||
def css_from(options)
|
||||
style = options.include?('tile_style') ? options['tile_style'] : nil
|
||||
(style.nil? || style.strip.empty?) ? EMPTY_CSS : style
|
||||
end
|
||||
|
||||
def wrap(query, options)
|
||||
wrapper = options.fetch('query_wrapper', nil)
|
||||
return query if wrapper.nil? || wrapper.empty?
|
||||
EJS.evaluate(wrapper, sql: query)
|
||||
end
|
||||
|
||||
def default_query_for(layer_options)
|
||||
if viewer_is_owner?
|
||||
"select * from #{safe_table_name_quoting(layer_options['table_name'])}"
|
||||
else
|
||||
"select * from #{qualify_table_name}"
|
||||
end
|
||||
end
|
||||
|
||||
def public_options
|
||||
return @configuration if @configuration.empty?
|
||||
@configuration.fetch(:layer_opts).fetch('public_opts')
|
||||
end
|
||||
|
||||
def whitelisted_infowindow(infowindow)
|
||||
infowindow.nil? ? nil : infowindow.select { |key, value|
|
||||
INFOWINDOW_KEYS.include?(key) || INFOWINDOW_KEYS.include?(key.to_s)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Used to migrate `wizard_properties` to `style_properties`
|
||||
class StylePropertiesGenerator
|
||||
def initialize(layer)
|
||||
@layer = layer
|
||||
@wizard_properties = @layer.options['wizard_properties']
|
||||
@source_type = @wizard_properties.present? ? @wizard_properties['type'] : nil
|
||||
end
|
||||
|
||||
def migrate(options)
|
||||
return nil unless @wizard_properties.present?
|
||||
|
||||
wpp = @wizard_properties['properties']
|
||||
|
||||
type = if @source_type == 'density'
|
||||
wpp['geometry_type'] == 'Rectangles' ? 'squares' : 'hexabins'
|
||||
elsif @source_type == 'torque_heat'
|
||||
wpp['heat-animated'] ? 'animation' : 'heatmap'
|
||||
else
|
||||
STYLE_PROPERTIES_TYPE[@source_type]
|
||||
end
|
||||
return nil unless type
|
||||
|
||||
options['cartocss_custom'] = options['tile_style_custom'] || false
|
||||
options['cartocss_history'] = options['tile_style_history'] || []
|
||||
|
||||
options['style_properties'] = {
|
||||
'autogenerated' => true,
|
||||
'type' => type,
|
||||
'properties' => wizard_properties_properties_to_style_properties_properties(wpp, type)
|
||||
}
|
||||
|
||||
merge_into_if_present(options['style_properties']['properties'], 'aggregation', generate_aggregation(wpp))
|
||||
|
||||
if SOURCE_TYPES_WITH_SQL_WRAP.include?(@source_type)
|
||||
options['sql_wrap'] = options['query_wrapper']
|
||||
end
|
||||
|
||||
if @source_type == 'cluster'
|
||||
options['cartocss_custom'] = true
|
||||
end
|
||||
|
||||
if type == 'animation' && @layer.widgets.where(type: 'time-series').none?
|
||||
create_time_series_widget(wpp)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
STYLE_PROPERTIES_TYPE = {
|
||||
'polygon' => 'simple',
|
||||
'bubble' => 'simple',
|
||||
'choropleth' => 'simple',
|
||||
'category' => 'simple',
|
||||
'torque' => 'animation',
|
||||
'torque_cat' => 'animation',
|
||||
'cluster' => 'simple'
|
||||
}.freeze
|
||||
|
||||
SOURCE_TYPES_WITH_SQL_WRAP = ['cluster', 'density', 'torque_cat'].freeze
|
||||
|
||||
def set_if_present(hash, key, value)
|
||||
# Dirty check because `false` is a valid `value`
|
||||
hash[key] = value if value.present?
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
def merge_into_if_present(hash, key, hash_value)
|
||||
hash[key] = hash_value.deep_merge!(hash[key] || {}) if hash_value.present?
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
def apply_direct_mapping(hash, original_hash, mapping)
|
||||
mapping.each do |source, target|
|
||||
set_if_present(hash, target, original_hash[source])
|
||||
end
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
def apply_default_opacity(hash)
|
||||
if hash['fixed'] && !hash['opacity']
|
||||
hash['opacity'] = 1
|
||||
end
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
PROPERTIES_DIRECT_MAPPING = {
|
||||
"marker-comp-op" => "blending",
|
||||
"torque-blend-mode" => "blending",
|
||||
"line-comp-op" => "blending"
|
||||
}.freeze
|
||||
|
||||
BLENDING_ALIAS = {
|
||||
'source-over' => 'src-over'
|
||||
}.freeze
|
||||
|
||||
ANIMATED_TYPES = ['animation', 'heatmap'].freeze
|
||||
|
||||
def wizard_properties_properties_to_style_properties_properties(wizard_properties_properties, type)
|
||||
spp = {}
|
||||
wpp = wizard_properties_properties
|
||||
return spp unless wpp
|
||||
|
||||
apply_direct_mapping(spp, wpp, PROPERTIES_DIRECT_MAPPING)
|
||||
|
||||
if spp['blending'].blank?
|
||||
spp['blending'] = 'none'
|
||||
else
|
||||
spp['blending'] = BLENDING_ALIAS.fetch(spp['blending'], spp['blending'])
|
||||
end
|
||||
|
||||
merge_into_if_present(spp, 'stroke', generate_stroke(wpp))
|
||||
|
||||
merge_into_if_present(spp, drawing_property(wpp), generate_drawing_properties(wpp))
|
||||
|
||||
merge_into_if_present(spp, 'labels', generate_labels(wpp))
|
||||
|
||||
if ANIMATED_TYPES.include?(type)
|
||||
merge_into_if_present(spp, 'animated', generate_animated(wpp))
|
||||
end
|
||||
|
||||
set_animated_style(spp) if type == 'animation'
|
||||
|
||||
set_property(spp, wpp)
|
||||
|
||||
spp
|
||||
end
|
||||
|
||||
def drawing_property(wpp)
|
||||
wpp['geometry_type'] == 'line' ? 'stroke' : 'fill'
|
||||
end
|
||||
|
||||
def set_property(spp, wpp)
|
||||
return unless wpp['property']
|
||||
|
||||
drawing_property = drawing_property(wpp)
|
||||
|
||||
destination = case @source_type
|
||||
when 'bubble'
|
||||
spp[drawing_property]['size']
|
||||
when 'choropleth', 'category'
|
||||
spp[drawing_property]['color']
|
||||
when 'torque', 'torque_heat', 'torque_cat'
|
||||
spp['animated']
|
||||
when 'density'
|
||||
spp['aggregation']['value']
|
||||
else
|
||||
# Ignore some malformed wizards that have a property set even when the type does not support it
|
||||
return
|
||||
end
|
||||
|
||||
destination['attribute'] = wpp['property']
|
||||
end
|
||||
|
||||
def set_animated_style(spp)
|
||||
spp['style'] = @source_type == 'torque_heat' ? 'heatmap' : 'simple'
|
||||
end
|
||||
|
||||
def generate_drawing_properties(wpp)
|
||||
fill = {}
|
||||
|
||||
merge_into_if_present(fill, @source_type == 'bubble' ? 'size' : 'color', generate_dimension_properties(wpp))
|
||||
|
||||
case @source_type
|
||||
when 'polygon', 'torque', 'torque_cat'
|
||||
fill['size'] = { 'fixed' => wpp['marker-width'] }.merge(fill['size'] || {})
|
||||
when 'choropleth', 'category'
|
||||
fill['size'] = { 'fixed' => 10 }.merge(fill['size'] || {})
|
||||
when 'torque_heat'
|
||||
fill['size'] = { 'fixed' => 35 }.merge(fill['size'] || {})
|
||||
end
|
||||
|
||||
merge_into_if_present(fill, 'color', generate_color(wpp))
|
||||
|
||||
fill
|
||||
end
|
||||
|
||||
STROKE_FROM_POINT_MAPPING = {
|
||||
'size' => {
|
||||
"marker-line-width" => 'fixed'
|
||||
},
|
||||
'color' => {
|
||||
"marker-line-color" => 'fixed',
|
||||
"marker-line-opacity" => 'opacity'
|
||||
}
|
||||
}.freeze
|
||||
|
||||
STROKE_FROM_NON_POINT_MAPPING = {
|
||||
'size' => {
|
||||
"line-width" => 'fixed'
|
||||
},
|
||||
'color' => {
|
||||
"line-color" => 'fixed',
|
||||
"line-opacity" => 'opacity'
|
||||
}
|
||||
}.freeze
|
||||
|
||||
def generate_stroke(wpp)
|
||||
stroke_mapping = if wpp['geometry_type'] == 'point' && @source_type != 'density'
|
||||
STROKE_FROM_POINT_MAPPING
|
||||
else
|
||||
STROKE_FROM_NON_POINT_MAPPING
|
||||
end
|
||||
|
||||
stroke = {}
|
||||
|
||||
merge_into_if_present(stroke, 'size', apply_direct_mapping({}, wpp, stroke_mapping['size']))
|
||||
merge_into_if_present(stroke, 'color', apply_direct_mapping({}, wpp, stroke_mapping['color']))
|
||||
|
||||
stroke
|
||||
end
|
||||
|
||||
TORQUE_HEAT_COLOR_DEFAULTS = {
|
||||
'attribute' => 'points_agg',
|
||||
'range' => ['blue', 'cyan', 'lightgreen', 'yellow', 'orange', 'red'],
|
||||
'bins' => 6
|
||||
}.freeze
|
||||
|
||||
def generate_color(wpp)
|
||||
color = {}
|
||||
|
||||
%w(polygon marker).each do |prefix|
|
||||
set_if_present(color, 'fixed', wpp["#{prefix}-fill"])
|
||||
|
||||
unless color['opacity']
|
||||
set_if_present(color, 'opacity', wpp["#{prefix}-opacity"])
|
||||
end
|
||||
|
||||
apply_default_opacity(color)
|
||||
end
|
||||
|
||||
if wpp['categories'].present?
|
||||
color['range'] = wpp['categories'].map { |c| c['color'] }
|
||||
color['domain'] = wpp['categories'].map { |c| c['title'] }
|
||||
end
|
||||
|
||||
color_attribute = if wpp['property_cat']
|
||||
wpp['property_cat']
|
||||
elsif @source_type == 'density'
|
||||
'agg_value'
|
||||
end
|
||||
color['attribute'] = color_attribute if color_attribute
|
||||
|
||||
color.merge!(TORQUE_HEAT_COLOR_DEFAULTS) if @source_type == 'torque_heat'
|
||||
|
||||
color
|
||||
end
|
||||
|
||||
SIZE_DIRECT_MAPPING = {
|
||||
'qfunction' => 'quantification'
|
||||
}.freeze
|
||||
|
||||
QUANTIFICATION_MAPPING = {
|
||||
'Jenks' => 'jenks',
|
||||
'Equal Interval' => 'equal',
|
||||
'Heads/Tails' => 'headtails',
|
||||
'Quantile' => 'quantiles'
|
||||
}.freeze
|
||||
|
||||
COLOR_RANGE_SOURCE_TYPES = ['choropleth', 'density'].freeze
|
||||
|
||||
def generate_dimension_properties(wpp)
|
||||
size = {}
|
||||
|
||||
radius_min = wpp['radius_min']
|
||||
radius_max = wpp['radius_max']
|
||||
if radius_min && radius_max
|
||||
size['range'] = [radius_min, radius_max]
|
||||
end
|
||||
|
||||
apply_direct_mapping(size, wpp, SIZE_DIRECT_MAPPING)
|
||||
quantification = size['quantification']
|
||||
size['quantification'] = QUANTIFICATION_MAPPING.fetch(quantification, quantification) if quantification.present?
|
||||
|
||||
if %w{ bubble category }.include?(@source_type)
|
||||
size['bins'] = 10
|
||||
end
|
||||
|
||||
if COLOR_RANGE_SOURCE_TYPES.include?(@source_type)
|
||||
# Commented because it might not be definitive
|
||||
# size['range'] = colorbrewer_ramp_array_from_color_ramp(wpp['color_ramp'])
|
||||
size['range'] = wpp['color_ramp']
|
||||
size['bins'] = extract_bins_from_method(wpp['method']).to_i
|
||||
end
|
||||
|
||||
size
|
||||
end
|
||||
|
||||
ANIMATED_DIRECT_MAPPING = {
|
||||
'torque-cumulative' => 'overlap',
|
||||
'torque-duration' => 'duration',
|
||||
'torque-frame-count' => 'steps',
|
||||
'torque-resolution' => 'resolution',
|
||||
'torque-trails' => 'trails'
|
||||
}.freeze
|
||||
|
||||
DEFAULT_ANIMATED = {
|
||||
'attribute' => nil,
|
||||
'overlap' => false,
|
||||
'duration' => 30,
|
||||
'steps' => 256,
|
||||
'resolution' => 2,
|
||||
'trails' => 2
|
||||
}.freeze
|
||||
|
||||
def generate_animated(wpp)
|
||||
animated = {}
|
||||
|
||||
apply_direct_mapping(animated, wpp, ANIMATED_DIRECT_MAPPING)
|
||||
|
||||
DEFAULT_ANIMATED.merge(animated)
|
||||
end
|
||||
|
||||
AGGREGATION_SOURCE_TYPES = %w{ density torque_heat }.freeze
|
||||
|
||||
def generate_aggregation(wpp)
|
||||
return {} unless AGGREGATION_SOURCE_TYPES.include?(@source_type)
|
||||
|
||||
size = case @source_type
|
||||
when 'density'
|
||||
wpp['polygon-size'] || 100
|
||||
when 'torque_heat'
|
||||
wpp['torque-resolution']
|
||||
else
|
||||
raise "Unsupported source type for aggregation: #{@source_type}"
|
||||
end
|
||||
|
||||
{
|
||||
"size" => size,
|
||||
"value" => {
|
||||
"operator" => 'COUNT',
|
||||
"attribute" => ''
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# Taken from `lib/assets/javascripts/cartodb/models/color_ramps.js`
|
||||
COLOR_ARRAYS_FROM_RAMPS = {
|
||||
'pink' => "['#E7E1EF', '#C994C7', '#DD1C77']",
|
||||
'red' => "['#FFEDA0', '#FEB24C', '#F03B20']",
|
||||
'black' => "['#F0F0F0', '#BDBDBD', '#636363']",
|
||||
'green' => "['#E5F5F9', '#99D8C9', '#2CA25F']",
|
||||
'blue' => "['#EDF8B1', '#7FCDBB', '#2C7FB8']",
|
||||
'inverted_pink' => "['#DD1C77','#C994C7','#E7E1EF']",
|
||||
'inverted_red' => "['#F03B20','#FEB24C','#FFEDA0']",
|
||||
'inverted_black' => "['#636363','#BDBDBD','#F0F0F0']",
|
||||
'inverted_green' => "['#2CA25F','#99D8C9','#E5F5F9']",
|
||||
'inverted_blue' => "['#2C7FB8','#7FCDBB','#EDF8B1']",
|
||||
'spectrum1' => "['#1a9850', '#fff2cc', '#d73027']",
|
||||
'spectrum2' => "['#0080ff', '#fff2cc', '#ff4d4d']",
|
||||
'blue_states' => "['#ECF0F6', '#6182B5', '#43618F']",
|
||||
'purple_states' => "['#F1E6F1', '#B379B3', '#8A4E8A']",
|
||||
'red_states' => "['#F2D2D3', '#D4686C', '#C1373C']",
|
||||
'inverted_blue_states' => "['#43618F', '#6182B5', '#ECF0F6']",
|
||||
'inverted_purple_states' => "['#8A4E8A', '#B379B3', '#F1E6F1']",
|
||||
'inverted_red_states' => "['#C1373C', '#D4686C', '#F2D2D3']"
|
||||
}.freeze
|
||||
|
||||
def colorbrewer_ramp_array_from_color_ramp(ramp)
|
||||
return [] unless ramp
|
||||
|
||||
COLOR_ARRAYS_FROM_RAMPS[ramp]
|
||||
end
|
||||
|
||||
DEFAULT_BINS = 6
|
||||
|
||||
def extract_bins_from_method(method)
|
||||
return DEFAULT_BINS unless method
|
||||
|
||||
number_match = method.match(/(\d*) Buckets/i)
|
||||
number_match && number_match[1] ? number_match[1] : DEFAULT_BINS
|
||||
end
|
||||
|
||||
TEXT_DIRECT_MAPPING = {
|
||||
'text-name' => 'attribute',
|
||||
'text-face-name' => 'font',
|
||||
'text-dy' => 'offset',
|
||||
'text-allow-overlap' => 'overlap',
|
||||
'text-placement-type' => 'placement'
|
||||
}.freeze
|
||||
|
||||
DEFAULT_LABELS = {
|
||||
'enabled' => false,
|
||||
'attribute' => nil,
|
||||
'font' => 'DejaVu Sans Book',
|
||||
'fill' => {
|
||||
'size' => {
|
||||
'fixed' => 10
|
||||
},
|
||||
'color' => {
|
||||
'fixed' => '#000',
|
||||
'opacity' => 1
|
||||
}
|
||||
},
|
||||
'halo' => {
|
||||
'size' => {
|
||||
'fixed' => 1
|
||||
},
|
||||
'color' => {
|
||||
'fixed' => '#111',
|
||||
'opacity' => 1
|
||||
}
|
||||
},
|
||||
'offset' => -10,
|
||||
'overlap' => true,
|
||||
'placement' => 'point'
|
||||
}.freeze
|
||||
|
||||
def generate_labels(wpp)
|
||||
labels = {}
|
||||
|
||||
apply_direct_mapping(labels, wpp, TEXT_DIRECT_MAPPING)
|
||||
labels['attribute'] = nil if labels['attribute'].to_s.downcase == 'none'
|
||||
|
||||
merge_into_if_present(labels, 'fill', generate_labels_fill(wpp))
|
||||
merge_into_if_present(labels, 'halo', generate_labels_halo(wpp))
|
||||
|
||||
labels['enabled'] = labels['attribute'].present?
|
||||
|
||||
DEFAULT_LABELS.merge(labels)
|
||||
end
|
||||
|
||||
def generate_labels_fill(wpp)
|
||||
labels_fill = {}
|
||||
|
||||
merge_into_if_present(labels_fill, 'size', generate_labels_fill_size(wpp))
|
||||
merge_into_if_present(labels_fill, 'color', generate_labels_fill_color(wpp))
|
||||
|
||||
labels_fill
|
||||
end
|
||||
|
||||
TEXT_SIZE_DIRECT_MAPPING = {
|
||||
'text-size' => 'fixed'
|
||||
}.freeze
|
||||
|
||||
TEXT_COLOR_DIRECT_MAPPING = {
|
||||
'text-fill' => 'fixed',
|
||||
'text-opacity' => 'opacity'
|
||||
}.freeze
|
||||
|
||||
def generate_labels_fill_size(wpp)
|
||||
size = {}
|
||||
|
||||
apply_direct_mapping(size, wpp, TEXT_SIZE_DIRECT_MAPPING)
|
||||
|
||||
size
|
||||
end
|
||||
|
||||
def generate_labels_fill_color(wpp)
|
||||
color = {}
|
||||
|
||||
apply_direct_mapping(color, wpp, TEXT_COLOR_DIRECT_MAPPING)
|
||||
apply_default_opacity(color)
|
||||
|
||||
color
|
||||
end
|
||||
|
||||
def generate_labels_halo(wpp)
|
||||
labels_halo = {}
|
||||
|
||||
merge_into_if_present(labels_halo, 'size', generate_labels_halo_size(wpp))
|
||||
merge_into_if_present(labels_halo, 'color', generate_labels_halo_color(wpp))
|
||||
|
||||
labels_halo
|
||||
end
|
||||
|
||||
HALO_SIZE_DIRECT_MAPPING = {
|
||||
'text-halo-radius' => 'fixed'
|
||||
}.freeze
|
||||
|
||||
HALO_COLOR_DIRECT_MAPPING = {
|
||||
'text-halo-fill' => 'fixed',
|
||||
'text-halo-opacity' => 'opacity'
|
||||
}.freeze
|
||||
|
||||
def generate_labels_halo_size(wpp)
|
||||
size = {}
|
||||
|
||||
apply_direct_mapping(size, wpp, HALO_SIZE_DIRECT_MAPPING)
|
||||
|
||||
size
|
||||
end
|
||||
|
||||
def generate_labels_halo_color(wpp)
|
||||
color = {}
|
||||
|
||||
apply_direct_mapping(color, wpp, HALO_COLOR_DIRECT_MAPPING)
|
||||
apply_default_opacity(color)
|
||||
|
||||
color
|
||||
end
|
||||
|
||||
def create_time_series_widget(wpp)
|
||||
if wpp['property'] && @layer.options[:source]
|
||||
@layer.widgets.create(
|
||||
type: 'time-series',
|
||||
order: 0,
|
||||
title: 'time_date__t',
|
||||
options: {
|
||||
column: wpp['property'],
|
||||
bins: 256,
|
||||
sync_on_data_change: true,
|
||||
sync_on_bbox_change: true
|
||||
},
|
||||
source_id: @layer.options[:source]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class LayerVizJSONAdapter
|
||||
extend Forwardable
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
delegate [:options, :kind, :id, :order, :legend, :user] => :layer
|
||||
|
||||
attr_reader :layer
|
||||
|
||||
def initialize(layer)
|
||||
@layer = layer
|
||||
end
|
||||
|
||||
def public_values
|
||||
{
|
||||
'options' => options,
|
||||
|
||||
# TODO: kind should be renamed to type
|
||||
# rename once a new layer presenter is written. See CartoDB::LayerModule::Presenter#with_kind_as_type
|
||||
# TODO: use symbols instead of strings
|
||||
'kind' => kind,
|
||||
|
||||
'infowindow' => infowindow,
|
||||
'tooltip' => tooltip,
|
||||
'id' => id,
|
||||
'order' => order
|
||||
}
|
||||
end
|
||||
|
||||
def get_presenter(options, configuration)
|
||||
Carto::Api::LayerPresenter.new(self, options, configuration)
|
||||
end
|
||||
|
||||
def infowindow
|
||||
@layer.infowindow
|
||||
end
|
||||
|
||||
def tooltip
|
||||
@layer.tooltip
|
||||
end
|
||||
|
||||
def infowindow_template_path
|
||||
if infowindow.present? && infowindow['template_name'].present?
|
||||
template_name = TEMPLATES_MAP.fetch(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 tooltip.present? && tooltip['template_name'].present?
|
||||
template_name = TEMPLATES_MAP.fetch(tooltip['template_name'], tooltip['template_name'])
|
||||
Rails.root.join("lib/assets/javascripts/cartodb/table/views/tooltip/templates/#{template_name}.jst.mustache")
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,283 @@
|
||||
|
||||
require_dependency 'carto/uuidhelper'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class LayersController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :show, :layers_by_map, :custom_layers_by_user, :map_index, :user_index, :map_show, :user_show,
|
||||
:map_create, :user_create, :map_update, :user_update, :map_destroy, :user_destroy
|
||||
|
||||
before_filter :ensure_current_user, only: [:user_index, :user_show, :user_create, :user_update, :user_destroy]
|
||||
before_filter :load_user_layer, only: [:user_show, :user_destroy]
|
||||
before_filter :load_user_layers, only: [:user_update]
|
||||
|
||||
before_filter :load_map, only: [:map_index, :map_show, :map_create, :map_update, :map_destroy]
|
||||
before_filter :ensure_writable_map, only: [:map_create, :map_update, :map_destroy]
|
||||
before_filter :load_map_layer, only: [:map_show, :map_destroy]
|
||||
before_filter :load_map_layers, only: [:map_update]
|
||||
|
||||
rescue_from LoadError,
|
||||
UnprocesableEntityError,
|
||||
UnauthorizedError, with: :rescue_from_carto_error
|
||||
|
||||
def map_index
|
||||
index(@map.layers)
|
||||
end
|
||||
|
||||
def user_index
|
||||
index(@user.layers, owner: @user)
|
||||
end
|
||||
|
||||
def map_show
|
||||
show(@map.user)
|
||||
end
|
||||
|
||||
def user_show
|
||||
show(current_user)
|
||||
end
|
||||
|
||||
def map_create
|
||||
layer = Carto::Layer.new(layer_attributes(params))
|
||||
validate_for_map(layer)
|
||||
|
||||
save_layer(layer) do
|
||||
@map.layers << layer
|
||||
@map.process_privacy_in(layer)
|
||||
|
||||
from_layer = Carto::Layer.where(id: params[:from_layer_id]).first if params[:from_layer_id]
|
||||
from_letter = params[:from_letter]
|
||||
update_layer_node_styles(layer, from_layer, from_letter)
|
||||
end
|
||||
end
|
||||
|
||||
def user_create
|
||||
layer = Carto::Layer.new(layer_attributes(params))
|
||||
|
||||
save_layer(layer) { @user.layers << layer }
|
||||
end
|
||||
|
||||
def map_update
|
||||
update
|
||||
end
|
||||
|
||||
def user_update
|
||||
update
|
||||
end
|
||||
|
||||
def map_destroy
|
||||
destroy
|
||||
end
|
||||
|
||||
def user_destroy
|
||||
destroy
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_for_map(layer)
|
||||
unless @map.can_add_layer?(current_user, layer)
|
||||
raise UnprocesableEntityError.new('Cannot add more layers to this visualization')
|
||||
end
|
||||
unless @map.admits_layer?(layer)
|
||||
raise UnprocesableEntityError.new('Cannot add more layers of this type')
|
||||
end
|
||||
|
||||
table_name = layer.options['table_name']
|
||||
user_name = layer.options['user_name']
|
||||
if user_name.present?
|
||||
table_name = user_name + '.' + table_name
|
||||
end
|
||||
|
||||
if layer.data_layer?
|
||||
table_visualization = Helpers::TableLocator.new.get_by_id_or_name(
|
||||
table_name,
|
||||
current_user
|
||||
).visualization
|
||||
unless table_visualization.has_read_permission?(current_user)
|
||||
raise UnauthorizedError.new('You do not have permission in the layer you are trying to add')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def index(layers, owner: nil)
|
||||
presented_layers = layers.map do |layer|
|
||||
Carto::Api::LayerPresenter.new(layer, viewer_user: current_user, user: owner || owner_user(layer)).to_poro
|
||||
end
|
||||
|
||||
render_jsonp layers: presented_layers, total_entries: presented_layers.size
|
||||
end
|
||||
|
||||
def show(owner)
|
||||
render_jsonp Carto::Api::LayerPresenter.new(@layer, viewer_user: current_user, user: owner).to_json
|
||||
end
|
||||
|
||||
# Takes a block, executed after saving
|
||||
def save_layer(layer)
|
||||
if layer.save
|
||||
yield
|
||||
|
||||
render_jsonp Carto::Api::LayerPresenter.new(layer, viewer_user: current_user).to_poro
|
||||
else
|
||||
CartoDB::Logger.error(message: 'Error creating layer', errors: layer.errors.full_messages)
|
||||
raise UnprocesableEntityError.new(layer.errors.full_messages)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
layers = @layers.map do |layer|
|
||||
layer_params = params[:layers].present? ? params[:layers].find { |p| p['id'] == layer.id } : params
|
||||
|
||||
# don't allow to override table_name and user_name
|
||||
new_layer_options = layer_params[:options]
|
||||
['table_name', 'user_name'].each do |key|
|
||||
if layer.options.include?(key)
|
||||
new_layer_options[key] = layer.options[key]
|
||||
else
|
||||
new_layer_options.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
unless layer.update_attributes(layer_attributes(layer_params))
|
||||
raise UnprocesableEntityError.new(layer.errors.full_messages)
|
||||
end
|
||||
|
||||
layer
|
||||
end
|
||||
|
||||
if layers.count > 1
|
||||
render_jsonp(layers: layers.map { |l| Carto::Api::LayerPresenter.new(l, viewer_user: current_user).to_poro })
|
||||
else
|
||||
render_jsonp Carto::Api::LayerPresenter.new(layers[0], viewer_user: current_user).to_poro
|
||||
end
|
||||
rescue RuntimeError => e
|
||||
CartoDB::Logger.error(message: 'Error updating layer', exception: e)
|
||||
render_jsonp({ description: e.message }, 400)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@layer.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def layer_attributes(param)
|
||||
param.slice(:options, :kind, :infowindow, :tooltip, :order).permit!
|
||||
end
|
||||
|
||||
def ensure_current_user
|
||||
user_id = uuid_parameter(:user_id)
|
||||
raise UnauthorizedError unless current_user.id == user_id
|
||||
@user = Carto::User.find(user_id)
|
||||
end
|
||||
|
||||
def load_user_layer
|
||||
load_user_layers
|
||||
raise LoadError.new('Layer not found') unless @layers.length == 1
|
||||
@layer = @layers.first
|
||||
end
|
||||
|
||||
def load_user_layers
|
||||
@layers = layers_ids.map { |id| @user.layers.find(id) }
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise LoadError.new('Layer not found')
|
||||
end
|
||||
|
||||
def load_map
|
||||
map_id = uuid_parameter(:map_id)
|
||||
|
||||
# User must be owner or have permissions for the map's visualization
|
||||
@map = Carto::Map.find(map_id)
|
||||
vis = @map.visualization
|
||||
raise LoadError.new('Map not found') unless vis.try(:is_viewable_by_user?, current_user)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise LoadError.new('Map not found')
|
||||
end
|
||||
|
||||
def ensure_writable_map
|
||||
raise UnauthorizedError unless @map.visualization.writable_by?(current_user)
|
||||
end
|
||||
|
||||
def load_map_layer
|
||||
load_map_layers
|
||||
raise LoadError.new('Layer not found') unless @layers.length == 1
|
||||
@layer = @layers.first
|
||||
end
|
||||
|
||||
def load_map_layers
|
||||
@layers = layers_ids.map { |id| @map.layers.find(id) }
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise LoadError.new('Layer not found')
|
||||
end
|
||||
|
||||
def layers_ids
|
||||
if params[:id]
|
||||
[params[:id]]
|
||||
elsif params[:layers]
|
||||
params[:layers].map { |l| l['id'] }
|
||||
else
|
||||
raise LoadError.new('Layer not found')
|
||||
end
|
||||
end
|
||||
|
||||
def owner_user(layer)
|
||||
if current_user.nil? || @map.user.id != current_user.id
|
||||
# This keeps backwards compatibility with map user assignment. See #8974
|
||||
@map.user
|
||||
elsif layer.options && layer.options['user_name'].present?
|
||||
::User.where(username: layer.options['user_name']).first
|
||||
else
|
||||
layer.user
|
||||
end
|
||||
end
|
||||
|
||||
def update_layer_node_styles(to_layer, from_layer, from_letter)
|
||||
to_letter = to_layer.options['letter']
|
||||
to_source = to_layer.options['source']
|
||||
if from_layer.present? && from_letter.present? && to_letter.present? && to_source.present?
|
||||
move_layer_node_styles(from_layer, from_letter, to_layer, to_letter, to_source)
|
||||
update_source_layer_styles(from_layer, from_letter, to_letter, to_source)
|
||||
end
|
||||
rescue => e
|
||||
CartoDB::Logger.error(
|
||||
message: 'Error updating layer node styles',
|
||||
exception: e,
|
||||
from_layer: from_layer,
|
||||
from_letter: from_letter,
|
||||
to_layer: to_layer
|
||||
)
|
||||
end
|
||||
|
||||
def move_layer_node_styles(from_layer, from_letter, to_layer, to_letter, to_source)
|
||||
source_node_number = to_source[1..-1].to_i
|
||||
nodes_to_move = from_layer.layer_node_styles.select do |lns|
|
||||
lns.source_id.starts_with?(from_letter) && lns.source_id[1..-1].to_i < source_node_number
|
||||
end
|
||||
|
||||
nodes_to_move.each do |lns|
|
||||
# Move LayerNodeStyles from the old layer if given.
|
||||
lns.source_id = lns.source_id.gsub(from_letter, to_letter)
|
||||
to_layer.layer_node_styles << lns
|
||||
end
|
||||
end
|
||||
|
||||
def update_source_layer_styles(from_layer, from_letter, to_letter, to_source)
|
||||
if from_letter != to_letter
|
||||
# Dragging middle node: rename the moved node
|
||||
node_id_to_fix = to_source.gsub(to_letter, from_letter)
|
||||
style_node = ::LayerNodeStyle.where(layer_id: from_layer.id, source_id: node_id_to_fix).first
|
||||
if style_node
|
||||
style_node.source_id = to_source
|
||||
style_node.save
|
||||
end
|
||||
else
|
||||
# Dragging head node: remove unneeded old styles in the old layer
|
||||
from_layer.reload
|
||||
from_layer.layer_node_styles.select { |lns|
|
||||
lns.source_id.starts_with?(from_letter) && lns.source_id != to_source
|
||||
}.each(&:destroy)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
module Carto
|
||||
module Api
|
||||
class LegendPresenter
|
||||
def initialize(legend)
|
||||
@legend = legend
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
conf: @legend.conf,
|
||||
created_at: @legend.created_at,
|
||||
definition: @legend.definition,
|
||||
id: @legend.id,
|
||||
layer_id: @legend.layer_id,
|
||||
post_html: @legend.post_html,
|
||||
pre_html: @legend.pre_html,
|
||||
title: @legend.title,
|
||||
type: @legend.type,
|
||||
updated_at: @legend.updated_at
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
module Carto
|
||||
module Api
|
||||
class LegendsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :index, :show, :create, :update, :destroy
|
||||
|
||||
before_filter :load_layer,
|
||||
:owners_only
|
||||
before_filter :load_legend, only: [:show, :update, :destroy]
|
||||
|
||||
rescue_from Carto::LoadError,
|
||||
Carto::UnauthorizedError,
|
||||
Carto::UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
|
||||
def index
|
||||
legend_presentations = @layer.legends.map do |legend|
|
||||
LegendPresenter.new(legend).to_hash
|
||||
end
|
||||
|
||||
render_jsonp(legend_presentations, :ok)
|
||||
end
|
||||
|
||||
def show
|
||||
legend_presentation = LegendPresenter.new(@legend).to_hash
|
||||
render_jsonp(legend_presentation, :ok)
|
||||
end
|
||||
|
||||
def create
|
||||
legend_params_with_layer_id = legend_params.merge(layer_id: @layer.id)
|
||||
legend = Legend.create(legend_params_with_layer_id)
|
||||
|
||||
if legend.valid?
|
||||
legend_presentation = LegendPresenter.new(legend).to_hash
|
||||
render_jsonp(legend_presentation, :created)
|
||||
else
|
||||
error = legend.errors.full_messages.join(', ')
|
||||
raise Carto::UnprocesableEntityError.new(error)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@legend.update_attributes!(legend_params)
|
||||
|
||||
legend_presentation = LegendPresenter.new(@legend).to_hash
|
||||
render_jsonp(legend_presentation, :ok)
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
error = @legend.errors.full_messages.join(', ')
|
||||
raise Carto::UnprocesableEntityError.new(error)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@legend.destroy
|
||||
|
||||
render_jsonp({}, :no_content)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_layer
|
||||
@layer = Carto::Layer.find(params[:layer_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise Carto::LoadError.new('Layer not found')
|
||||
end
|
||||
|
||||
def owners_only
|
||||
visualization = Carto::Visualization.find(params[:visualization_id])
|
||||
|
||||
unless visualization.layers.include?(@layer) &&
|
||||
visualization.writable_by?(current_viewer)
|
||||
raise Carto::UnauthorizedError.new
|
||||
end
|
||||
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise Carto::LoadError.new('Visualization not found')
|
||||
end
|
||||
|
||||
def load_legend
|
||||
@legend = @layer.legends.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise Carto::LoadError.new('Legend not found')
|
||||
end
|
||||
|
||||
def legend_params
|
||||
params.slice(:title, :pre_html, :post_html, :type, :definition, :conf).permit!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
module Carto
|
||||
module Api
|
||||
class MapPresenter
|
||||
def initialize(map)
|
||||
@map = map
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
bounding_box_ne: @map.bounding_box_ne,
|
||||
bounding_box_sw: @map.bounding_box_sw,
|
||||
center: @map.center,
|
||||
options: @map.options,
|
||||
id: @map.id,
|
||||
provider: @map.provider,
|
||||
user_id: @map.user_id,
|
||||
view_bounds_ne: @map.view_bounds_ne,
|
||||
view_bounds_sw: @map.view_bounds_sw,
|
||||
zoom: @map.zoom,
|
||||
legends: @map.legends,
|
||||
scrollwheel: @map.scrollwheel
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
module Carto
|
||||
module Api
|
||||
class MapcapPresenter
|
||||
|
||||
def initialize(mapcap)
|
||||
@mapcap = mapcap
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return {} unless @mapcap
|
||||
|
||||
{
|
||||
id: @mapcap.id,
|
||||
related_ids: @mapcap.ids_json,
|
||||
created_at: @mapcap.created_at
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
require_relative '../builder/builder_users_module'
|
||||
|
||||
require_dependency 'carto/tracking/events'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class MapcapsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
include Carto::Builder::BuilderUsersModule
|
||||
|
||||
ssl_required :show, :create, :destroy, :index
|
||||
|
||||
before_filter :builder_users_only,
|
||||
:load_visualization,
|
||||
:owners_only
|
||||
before_filter :load_mapcap, only: [:show, :destroy]
|
||||
|
||||
after_filter :track_published_map, only: :create
|
||||
|
||||
rescue_from StandardError, with: :rescue_from_standard_error
|
||||
rescue_from Carto::LoadError,
|
||||
Carto::UnauthorizedError,
|
||||
Carto::UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
|
||||
def index
|
||||
render_jsonp(@visualization.mapcaps.map { |mapcap| Carto::Api::MapcapPresenter.new(mapcap).to_poro })
|
||||
end
|
||||
|
||||
def create
|
||||
mapcap = @visualization.create_mapcap!
|
||||
|
||||
render_jsonp(Carto::Api::MapcapPresenter.new(mapcap).to_poro, :created)
|
||||
rescue ActiveRecord::RecordInvalid => exception
|
||||
message = exception.record.errors.full_messages.join(', ')
|
||||
raise Carto::UnprocesableEntityError.new(message)
|
||||
end
|
||||
|
||||
def show
|
||||
render_jsonp(Carto::Api::MapcapPresenter.new(@mapcap).to_poro)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@mapcap.destroy
|
||||
|
||||
render_jsonp({}, :no_content)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_visualization
|
||||
visualization_id = uuid_parameter(:visualization_id)
|
||||
|
||||
@visualization = Carto::Visualization.find(visualization_id)
|
||||
|
||||
raise ActiveRecord::RecordNotFound if @visualization.canonical?
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise Carto::LoadError.new("Visualization not found: #{visualization_id}")
|
||||
end
|
||||
|
||||
def owners_only
|
||||
raise Carto::UnauthorizedError.new unless @visualization.writable_by?(current_user)
|
||||
end
|
||||
|
||||
def load_mapcap
|
||||
mapcap_id = uuid_parameter(:id)
|
||||
|
||||
@mapcap = Carto::Mapcap.find(mapcap_id)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise Carto::LoadError.new("Mapcap not found: #{mapcap_id}")
|
||||
end
|
||||
|
||||
def track_published_map
|
||||
current_viewer_id = current_viewer.id
|
||||
Carto::Tracking::Events::PublishedMap.new(current_viewer_id,
|
||||
user_id: current_viewer_id,
|
||||
visualization_id: @visualization.id).report
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
module Carto
|
||||
module Api
|
||||
class MapsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :show, :update
|
||||
|
||||
before_filter :load_map, :owners_only
|
||||
|
||||
rescue_from Carto::LoadError,
|
||||
Carto::UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
|
||||
def show
|
||||
render_jsonp(map_presentation)
|
||||
end
|
||||
|
||||
def update
|
||||
@map.update_attributes!(update_params)
|
||||
|
||||
render_jsonp(map_presentation)
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
validation_errors = @map.errors.full_messages.join(', ')
|
||||
raise Carto::UnprocesableEntityError.new(validation_errors)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_map
|
||||
@map = Carto::Map.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise Carto::LoadError.new('Map not found')
|
||||
end
|
||||
|
||||
def owners_only
|
||||
unless @map.writable_by_user?(current_viewer)
|
||||
raise Carto::LoadError.new('Map not found')
|
||||
end
|
||||
end
|
||||
|
||||
STRING_PARAMS = [:bounding_box_sw, :bounding_box_ne, :center, :view_bounds_sw, :view_bounds_ne].freeze
|
||||
|
||||
def update_params
|
||||
update_params = params.slice(:bounding_box_ne,
|
||||
:bounding_box_sw,
|
||||
:center,
|
||||
:options,
|
||||
:provider,
|
||||
:view_bounds_ne,
|
||||
:view_bounds_sw,
|
||||
:zoom,
|
||||
:legends,
|
||||
:scrollwheel).permit!
|
||||
|
||||
STRING_PARAMS.each do |param|
|
||||
update_params[param] = update_params[param].to_s
|
||||
end
|
||||
|
||||
# Remove empty values, keeping `false`s (`present?` can't be used because of this)
|
||||
update_params.reject { |_k, v| v.nil? || v == '' }
|
||||
end
|
||||
|
||||
def map_presentation
|
||||
Carto::Api::MapPresenter.new(@map).to_hash
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
module Carto
|
||||
module Api
|
||||
class MetricsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :create
|
||||
|
||||
skip_before_filter :api_authorization_required
|
||||
|
||||
before_filter :load_event, only: :create
|
||||
|
||||
rescue_from Carto::LoadError,
|
||||
Carto::UnauthorizedError,
|
||||
Carto::UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
|
||||
def create
|
||||
@event.report!
|
||||
|
||||
render json: Hash.new, status: :created
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_event
|
||||
event_name = params[:name]
|
||||
|
||||
raise Carto::UnprocesableEntityError.new('name not provided') unless event_name
|
||||
|
||||
modulized_name = "Carto::Tracking::Events::#{event_name.parameterize('_').camelize}"
|
||||
|
||||
@event = Carto::Tracking::Events::SegmentEvent.build(params[:name], current_viewer.try(:id), params[:properties])
|
||||
@event ||= modulized_name.constantize.new(current_viewer.try(:id), params[:properties])
|
||||
rescue NameError
|
||||
raise Carto::LoadError.new("Event not found: #{event_name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,99 @@
|
||||
module Carto
|
||||
module Api
|
||||
class MobileAppPresenter
|
||||
|
||||
def initialize(mobile_app, current_user, fetch_mobile_platforms, fetch_app_types)
|
||||
@mobile_app = mobile_app
|
||||
@current_user = current_user
|
||||
@fetch_mobile_platforms = fetch_mobile_platforms
|
||||
@fetch_app_types = fetch_app_types
|
||||
end
|
||||
|
||||
def data
|
||||
return {} if @mobile_app.nil?
|
||||
data = {
|
||||
id: @mobile_app.id,
|
||||
name: @mobile_app.name,
|
||||
description: @mobile_app.description,
|
||||
icon_url: @mobile_app.icon_url,
|
||||
platform: @mobile_app.platform,
|
||||
app_type: @mobile_app.app_type,
|
||||
app_id: @mobile_app.app_id,
|
||||
license_key: @mobile_app.license_key,
|
||||
monthly_users: @mobile_app.monthly_users
|
||||
}
|
||||
|
||||
if @fetch_mobile_platforms == true
|
||||
data[:mobile_platforms] = {
|
||||
"android": {
|
||||
text: "Android",
|
||||
available: platform_available?('android', true),
|
||||
selected: platform_selected?('android'),
|
||||
legend: "Use package from AndroidManifest.xml. E.g: com.example.mycartoapp."
|
||||
},
|
||||
"ios": {
|
||||
text: "iOS",
|
||||
available: platform_available?('ios', true),
|
||||
selected: platform_selected?('ios'),
|
||||
legend: "Use Bundle identifier. You can find it in the project properties. E.g: com.example.mycartoapp."
|
||||
},
|
||||
"xamarin-android": {
|
||||
text: "Xamarin Android",
|
||||
available: platform_available?('xamarin-android', @current_user.mobile_xamarin),
|
||||
selected: platform_selected?('xamarin-android'),
|
||||
legend: "Use package from AndroidManifest.xml. E.g: com.example.mycartoapp."
|
||||
},
|
||||
"xamarin-ios": {
|
||||
text: "Xamarin iOS",
|
||||
available: platform_available?('xamarin-ios', @current_user.mobile_xamarin),
|
||||
selected: platform_selected?('xamarin-ios'),
|
||||
legend: "Use Bundle identifier. You can find it in the project properties. E.g: com.example.mycartoapp."
|
||||
},
|
||||
"windows-phone": {
|
||||
text: "Windows Phone",
|
||||
available: platform_available?('windows-phone', @current_user.mobile_xamarin),
|
||||
selected: platform_selected?('windows-phone'),
|
||||
legend: "Use the Package name from Package.appmanifest. E.g: c882d38a-5c09-4994-87f0-89875cdee539."
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
if @fetch_app_types == true
|
||||
data[:app_types] = {
|
||||
"open": {
|
||||
text: "Limits based on your CARTO plan. <a href='https://carto.com/developers/fundamentals/limits/'
|
||||
target='_blank'>Learn more</a>.",
|
||||
available: app_type_available?('open', @current_user.open_apps_enabled?),
|
||||
selected: app_type_selected?('open')
|
||||
},
|
||||
"private": {
|
||||
text: "Only for enterprise. <a href='https://carto.com/pricing/' target='_blank'>Learn more</a>.",
|
||||
available: app_type_available?('private', @current_user.private_apps_enabled?),
|
||||
selected: app_type_selected?('private')
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def platform_available?(platform, current_platform_enabled)
|
||||
(!@mobile_app.persisted? && current_platform_enabled) || platform_selected?(platform)
|
||||
end
|
||||
|
||||
def platform_selected?(platform)
|
||||
@mobile_app.platform == platform
|
||||
end
|
||||
|
||||
def app_type_available?(app_type, current_apps_enabled)
|
||||
(!@mobile_app.persisted? && current_apps_enabled) || app_type_selected?(app_type) || (@mobile_app.persisted? && current_apps_enabled)
|
||||
end
|
||||
|
||||
def app_type_selected?(app_type)
|
||||
@mobile_app.app_type == app_type
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
module Carto
|
||||
module Api
|
||||
class MultifactorAuthPresenter
|
||||
def initialize(multifactor_auth)
|
||||
@multifactor_auth = multifactor_auth
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return {} unless @multifactor_auth
|
||||
|
||||
{
|
||||
id: @multifactor_auth.id,
|
||||
user: @multifactor_auth.user.username,
|
||||
type: @multifactor_auth.type,
|
||||
enabled: @multifactor_auth.enabled,
|
||||
created_at: @multifactor_auth.created_at.to_s,
|
||||
updated_at: @multifactor_auth.updated_at.to_s
|
||||
}
|
||||
end
|
||||
|
||||
def to_poro_with_qrcode
|
||||
to_poro.merge(
|
||||
qrcode: @multifactor_auth.qr_code
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
module Carto
|
||||
module Api
|
||||
class MultifactorAuthenticationController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
extend Carto::DefaultRescueFroms
|
||||
include OrganizationUsersHelper
|
||||
|
||||
ssl_required
|
||||
|
||||
before_action :load_organization
|
||||
before_action :admins_only
|
||||
before_action :load_user
|
||||
before_action :create_service
|
||||
before_action :ensure_edit_permissions
|
||||
|
||||
setup_default_rescues
|
||||
|
||||
def show
|
||||
render_jsonp({ mfa_required: @service.exists?(type: base_params[:type]) }, 200)
|
||||
end
|
||||
|
||||
def create
|
||||
if @service.exists?(type: base_params[:type])
|
||||
raise Carto::UnprocesableEntityError.new('Multi-factor authentication already exists')
|
||||
end
|
||||
|
||||
@service.update(enabled: true, type: base_params[:type])
|
||||
render_jsonp({ success: true }, 201)
|
||||
end
|
||||
|
||||
def destroy
|
||||
unless @service.exists?(type: base_params[:type])
|
||||
raise Carto::UnprocesableEntityError.new('Multi-factor authentication does not exist')
|
||||
end
|
||||
|
||||
@service.update(enabled: false, type: base_params[:type])
|
||||
render_jsonp({ success: true }, 204)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_params
|
||||
@base_params ||= params.permit([:type])
|
||||
end
|
||||
|
||||
def create_service
|
||||
@service = Carto::UserMultifactorAuthUpdateService.new(user_id: @user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,60 @@
|
||||
class Carto::Api::MultifactorAuthsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required
|
||||
|
||||
before_action :load_user
|
||||
|
||||
before_action :load_multifactor_auth, only: [:show, :verify_code, :destroy]
|
||||
|
||||
rescue_from Carto::UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UnauthorizedError, with: :rescue_from_carto_error
|
||||
|
||||
def create
|
||||
multifactor_auth = @carto_viewer.user_multifactor_auths.create!(create_params)
|
||||
render json: Carto::Api::MultifactorAuthPresenter.new(multifactor_auth).to_poro_with_qrcode, status: 201
|
||||
rescue ActionController::ParameterMissing => e
|
||||
raise Carto::UnprocesableEntityError.new(e.message)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
raise Carto::UnprocesableEntityError.new(e.message)
|
||||
end
|
||||
|
||||
def verify_code
|
||||
@multifactor_auth.verify!(params[:code])
|
||||
render json: Carto::Api::MultifactorAuthPresenter.new(@multifactor_auth).to_poro, status: 200
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
raise Carto::UnprocesableEntityError.new(e.message)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@multifactor_auth.destroy
|
||||
render json: Carto::Api::MultifactorAuthPresenter.new(@multifactor_auth).to_poro, status: 204
|
||||
end
|
||||
|
||||
def show
|
||||
render json: Carto::Api::MultifactorAuthPresenter.new(@multifactor_auth).to_poro, status: 200
|
||||
end
|
||||
|
||||
def index
|
||||
auths = @carto_viewer.user_multifactor_auths.map do |auth|
|
||||
Carto::Api::MultifactorAuthPresenter.new(auth).to_poro
|
||||
end
|
||||
|
||||
render json: auths, status: 200
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user
|
||||
@carto_viewer = Carto::User.find(current_viewer.id)
|
||||
end
|
||||
|
||||
def load_multifactor_auth
|
||||
@multifactor_auth = @carto_viewer.user_multifactor_auths.find(params[:id])
|
||||
end
|
||||
|
||||
def create_params
|
||||
{ type: params.require(:type) }
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
class Carto::Api::NotificationPresenter
|
||||
extend Forwardable
|
||||
include Carto::HtmlSafe
|
||||
|
||||
delegate [:id, :icon, :recipients, :body, :created_at] => :@notification
|
||||
|
||||
def initialize(notification)
|
||||
@notification = notification
|
||||
end
|
||||
|
||||
def html_body
|
||||
markdown_html_safe(@notification.body)
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
id: @notification.id,
|
||||
organization_id: @notification.organization_id,
|
||||
icon: @notification.icon,
|
||||
recipients: @notification.recipients,
|
||||
body: @notification.body,
|
||||
created_at: @notification.created_at
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,170 @@
|
||||
require 'uri'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class OembedController < ::Api::ApplicationController
|
||||
include VisualizationSearcher
|
||||
|
||||
ssl_allowed :show
|
||||
|
||||
skip_before_filter :api_authorization_required
|
||||
|
||||
# Returns oembed data as required
|
||||
def show
|
||||
url = params[:url]
|
||||
width = params[:maxwidth] || '100%'
|
||||
height = params[:maxheight] || '520px'
|
||||
format = request.query_parameters[:format]
|
||||
force_https = true if params[:allow_http].nil?
|
||||
|
||||
raise ActionController::RoutingError.new('Incorrect width') if (width =~ /\A[0-9]+(%|px)?\z/).nil?
|
||||
raise ActionController::RoutingError.new('Incorrect height') if (height =~ /\A[0-9]+(%|px)?\z/).nil?
|
||||
|
||||
uri = URI.parse(url)
|
||||
|
||||
uuid = extract_uuid_from_string(uri.path)
|
||||
raise ActionController::RoutingError.new('UUID not found in URL') if uuid.nil?
|
||||
|
||||
vis = Carto::Visualization.where(id: @id).first
|
||||
if !vis.nil?
|
||||
name = vis.name
|
||||
end
|
||||
|
||||
fields = url_fields_from_fragments(url, force_https)
|
||||
|
||||
# build the url using full schema because any visuaization should work with any user
|
||||
if fields[:organization_name].nil?
|
||||
url = CartoDB.base_url(fields[:username])
|
||||
else
|
||||
url = CartoDB.base_url(fields[:organization_name], fields[:username])
|
||||
end
|
||||
url += CartoDB.path(self, 'public_visualizations_embed_map', id: uuid, user_domain: nil)
|
||||
|
||||
# force the schema
|
||||
if fields[:protocol] == 'https' && !url.include?('https')
|
||||
url = url.sub('http', 'https')
|
||||
end
|
||||
|
||||
html = "<iframe width='#{width}' height='#{height}' frameborder='0' src='#{url}' allowfullscreen webkitallowfullscreen mozallowfullscreen oallowfullscreen msallowfullscreen></iframe>"
|
||||
|
||||
response_data = {
|
||||
:type => 'rich',
|
||||
:version => '1.0',
|
||||
:width => width,
|
||||
:height => height,
|
||||
:title => name,
|
||||
:html => html,
|
||||
:author_name => fields[:username],
|
||||
:author_url => fields[:user_profile_url],
|
||||
:provider_name => 'CartoDB',
|
||||
:provider_url => "#{fields[:protocol]}://carto.com/"
|
||||
}
|
||||
|
||||
if format == 'xml'
|
||||
render xml: response_data.to_xml(root: 'oembed')
|
||||
else
|
||||
render json: response_data.to_json, :callback => params['callback']
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def url_fields_from_fragments(url, force_https)
|
||||
domain = CartoDB.session_domain
|
||||
# @see http://ruby-doc.org/stdlib-1.9.3/libdoc/uri/rdoc/URI.html#method-c-split
|
||||
url_fragments = URI.split(url)
|
||||
protocol = force_https ? "https" : URI.parse(url).scheme
|
||||
|
||||
data = nil
|
||||
unless CartoDB.subdomainless_urls?
|
||||
begin
|
||||
data = from_url(url_fragments, protocol, domain)
|
||||
rescue UrlFRagmentsError
|
||||
# URL is subdomainless so do nothing
|
||||
end
|
||||
end
|
||||
|
||||
# Either subdomains disallowed or url doesn't uses them
|
||||
if data.nil?
|
||||
data = from_domainless_url(url_fragments, protocol)
|
||||
end
|
||||
|
||||
raise UrlFRagmentsError.new("Couldn't extract URL fields") if data.nil?
|
||||
|
||||
{
|
||||
organization_name: data[:organization_name],
|
||||
username: data[:username],
|
||||
user_profile_url: data[:user_profile_url],
|
||||
protocol: protocol
|
||||
}
|
||||
end
|
||||
|
||||
# testuser.carto.com || testorg.carto.com/u/user
|
||||
def from_url(url_fragments, protocol, domain)
|
||||
# To ease testing don't request eactly all URI.split params
|
||||
raise UrlFRagmentsError.new("Invalid url_fragments parameter") unless url_fragments.length > 5
|
||||
|
||||
subdomain = url_fragments[2].sub(domain, '.').split('.')[0]
|
||||
raise UrlFRagmentsError.new("Subdomain not found at url") if subdomain.nil?
|
||||
|
||||
# org-based
|
||||
if url_fragments[5][0..2] == "/u/"
|
||||
organization_name = subdomain
|
||||
username = username_from_url_fragments(url_fragments)
|
||||
else
|
||||
organization_name = nil
|
||||
username = subdomain
|
||||
end
|
||||
|
||||
{
|
||||
username: username,
|
||||
organization_name: organization_name,
|
||||
user_profile_url: CartoDB.base_url(subdomain, organization_name.nil? ? nil : username, protocol)
|
||||
}
|
||||
end
|
||||
|
||||
# https://carto.com/u/testuser/...
|
||||
def from_domainless_url(url_fragments, protocol)
|
||||
# To ease testing don't request eactly all URI.split params
|
||||
raise UrlFRagmentsError.new("Invalid url_fragments parameter") unless url_fragments.length > 5
|
||||
|
||||
# url_fragments[5]: Path
|
||||
raise UrlFRagmentsError.new("URL needs username specified in the Path") if url_fragments[5][0..2] != "/u/"
|
||||
|
||||
# url_fragments[3]: Host
|
||||
port_fragment =
|
||||
url_fragments[3].nil? || url_fragments[3] == '' || url_fragments[3].to_i == 80 ? '' : ":#{url_fragments[3]}"
|
||||
|
||||
username = username_from_url_fragments(url_fragments)
|
||||
{
|
||||
username: username,
|
||||
organization_name: nil,
|
||||
# url_fragments[2]: Host
|
||||
user_profile_url: "#{protocol}://#{url_fragments[2]}#{port_fragment}/u/#{username}"
|
||||
}
|
||||
end
|
||||
|
||||
def username_from_url_fragments(url_fragments)
|
||||
path_fragments = url_fragments[5].split('/')
|
||||
raise UrlFRagmentsError.new("Username not found at url") if path_fragments.length < 3 || path_fragments[2].length == 0
|
||||
path_fragments[2]
|
||||
end
|
||||
|
||||
def extract_uuid_from_string(str)
|
||||
# TODO: move this method to app/helpers/carto/uuidhelper.rb. Not used yet because this changed was pushed before
|
||||
# UUIDTools::UUID_REGEXP cannot be reused because of /^ $/
|
||||
matches = /([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{2})([0-9a-f]{2})-([0-9a-f]{12})/.match(str)
|
||||
if matches
|
||||
matches[0]
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class UrlFRagmentsError < StandardError
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
module Carto
|
||||
module Api
|
||||
class OrganizationAssetsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :index, :show, :create, :destroy
|
||||
|
||||
before_filter :load_organization,
|
||||
:organization_members_only
|
||||
before_filter :organization_owners_only, only: [:create, :destroy]
|
||||
before_filter :load_asset, only: [:show, :destroy]
|
||||
before_filter :load_resource, only: :create
|
||||
|
||||
rescue_from LoadError,
|
||||
UnprocesableEntityError,
|
||||
UnauthorizedError, with: :rescue_from_carto_error
|
||||
|
||||
def index
|
||||
presentation = @organization.assets.map do |asset|
|
||||
AssetPresenter.new(asset).to_hash
|
||||
end
|
||||
|
||||
render json: presentation
|
||||
end
|
||||
|
||||
def show
|
||||
render json: AssetPresenter.new(@asset).to_hash
|
||||
end
|
||||
|
||||
def create
|
||||
asset = Asset.for_organization(organization: @organization,
|
||||
resource: @resource)
|
||||
|
||||
asset.save!
|
||||
render json: AssetPresenter.new(asset), status: :created
|
||||
rescue ActiveRecord::RecordInvalid => exception
|
||||
raise UnprocesableEntityError.with_full_messages(exception)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@asset.destroy
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_organization
|
||||
@organization = Organization.find(params[:organization_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise LoadError.new('Organization not found')
|
||||
end
|
||||
|
||||
def organization_members_only
|
||||
unless current_viewer.belongs_to_organization?(@organization)
|
||||
raise UnauthorizedError.new
|
||||
end
|
||||
end
|
||||
|
||||
def organization_owners_only
|
||||
raise UnauthorizedError.new unless @organization.owner?(current_viewer)
|
||||
end
|
||||
|
||||
def load_asset
|
||||
@asset = Asset.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise LoadError.new('Asset not found')
|
||||
end
|
||||
|
||||
def load_resource
|
||||
@resource = params[:resource]
|
||||
unless @resource.present?
|
||||
raise UnprocesableEntityError.new('Missing resource for asset')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
module Carto
|
||||
module Api
|
||||
class OrganizationNotificationsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
extend Carto::DefaultRescueFroms
|
||||
|
||||
ssl_required :create, :destroy
|
||||
|
||||
before_filter :owners_only, :load_organization
|
||||
before_filter :load_notification, only: [:destroy]
|
||||
|
||||
setup_default_rescues
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
notification_parameters = params.require(:notification).permit(:icon, :body, :recipients)
|
||||
notification = @organization.notifications.create!(notification_parameters)
|
||||
render_jsonp(NotificationPresenter.new(notification).to_hash, :created)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@notification.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_organization
|
||||
@organization = Carto::Organization.find(current_user.organization_id)
|
||||
unless [@organization.name, @organization.id].include?(params[:organization_id])
|
||||
raise Carto::LoadError.new('Cannot find organization')
|
||||
end
|
||||
end
|
||||
|
||||
def owners_only
|
||||
raise Carto::UnauthorizedError.new('Only organization owners') unless current_user.organization_owner?
|
||||
end
|
||||
|
||||
def load_notification
|
||||
@notification = @organization.notifications.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
require_relative 'group_presenter'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class OrganizationPresenter
|
||||
|
||||
def initialize(organization)
|
||||
@organization = organization
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return {} if @organization.nil?
|
||||
filtered_user ||= @organization.owner
|
||||
{
|
||||
created_at: @organization.created_at,
|
||||
description: @organization.description,
|
||||
discus_shortname: @organization.discus_shortname,
|
||||
display_name: @organization.display_name,
|
||||
id: @organization.id,
|
||||
name: @organization.name,
|
||||
owner: {
|
||||
id: @organization.owner ? @organization.owner.id : nil,
|
||||
username: @organization.owner ? @organization.owner.username : nil,
|
||||
avatar_url: @organization.owner ? @organization.owner.avatar_url : nil,
|
||||
email: @organization.owner ? @organization.owner.email : nil,
|
||||
groups: @organization.owner && @organization.owner.groups ? @organization.owner.groups.map { |g| Carto::Api::GroupPresenter.new(g).to_poro } : []
|
||||
},
|
||||
admins: @organization.users.where(org_admin: true).map { |u| { id: u.id } },
|
||||
quota_in_bytes: @organization.quota_in_bytes,
|
||||
unassigned_quota: @organization.unassigned_quota,
|
||||
geocoding_quota: @organization.geocoding_quota,
|
||||
here_isolines_quota: @organization.here_isolines_quota,
|
||||
here_isolines_block_price: @organization.here_isolines_block_price,
|
||||
obs_snapshot_quota: @organization.obs_snapshot_quota,
|
||||
obs_snapshot_block_price: @organization.obs_snapshot_block_price,
|
||||
obs_general_quota: @organization.obs_general_quota,
|
||||
obs_general_block_price: @organization.obs_general_block_price,
|
||||
mapzen_routing_quota: @organization.mapzen_routing_quota,
|
||||
mapzen_routing_block_price: @organization.mapzen_routing_block_price,
|
||||
geocoder_provider: @organization.geocoder_provider,
|
||||
isolines_provider: @organization.isolines_provider,
|
||||
routing_provider: @organization.routing_provider,
|
||||
map_view_quota: @organization.map_view_quota,
|
||||
twitter_datasource_quota: @organization.twitter_datasource_quota,
|
||||
map_view_block_price: @organization.map_view_block_price,
|
||||
geocoding_block_price: @organization.geocoding_block_price,
|
||||
seats: @organization.seats,
|
||||
viewer_seats: @organization.viewer_seats,
|
||||
twitter_username: @organization.twitter_username,
|
||||
location: @organization.location,
|
||||
updated_at: @organization.updated_at,
|
||||
website: @organization.website,
|
||||
admin_email: @organization.admin_email,
|
||||
avatar_url: @organization.avatar_url,
|
||||
user_count: @organization.users.count,
|
||||
password_expiration_in_d: @organization.password_expiration_in_d
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
module Carto
|
||||
module Api
|
||||
class OrganizationPublicPresenter
|
||||
def initialize(organization)
|
||||
@organization = organization
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
name: @organization.name
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,176 @@
|
||||
require_relative './user_presenter'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class OrganizationUsersController < ::Api::ApplicationController
|
||||
include OrganizationUsersHelper
|
||||
|
||||
ssl_required :index, :show, :create, :update, :destroy
|
||||
|
||||
before_filter :load_organization
|
||||
before_filter :admins_only
|
||||
before_filter :load_user, only: [:show, :update, :destroy]
|
||||
before_filter :ensure_edit_permissions, only: [:show, :update, :destroy]
|
||||
|
||||
def index
|
||||
presentations = @organization.users.map do |user|
|
||||
Carto::Api::UserPresenter.new(user, current_viewer: current_viewer).to_eumapi_poro
|
||||
end
|
||||
|
||||
render_jsonp presentations, 200
|
||||
end
|
||||
|
||||
def show
|
||||
presentation = Carto::Api::UserPresenter.new(@user, current_viewer: current_viewer)
|
||||
.to_eumapi_poro
|
||||
|
||||
render_jsonp presentation, 200
|
||||
end
|
||||
|
||||
def create
|
||||
account_creator = CartoDB::UserAccountCreator.new(Carto::UserCreation::CREATED_VIA_API)
|
||||
.with_organization(@organization)
|
||||
|
||||
account_creator.with_username(create_params[:username]) if create_params[:username].present?
|
||||
account_creator.with_email(create_params[:email]) if create_params[:email].present?
|
||||
account_creator.with_password(create_params[:password]) if create_params[:password].present?
|
||||
account_creator.with_quota_in_bytes(create_params[:quota_in_bytes]) if create_params[:quota_in_bytes].present?
|
||||
|
||||
param_viewer = create_params[:viewer]
|
||||
account_creator.with_viewer(param_viewer) if param_viewer
|
||||
|
||||
param_org_admin = create_params[:org_admin]
|
||||
account_creator.with_org_admin(param_org_admin) if param_org_admin
|
||||
|
||||
if create_params[:soft_geocoding_limit].present?
|
||||
account_creator.with_soft_geocoding_limit(create_params[:soft_geocoding_limit])
|
||||
end
|
||||
|
||||
if create_params[:soft_here_isolines_limit].present?
|
||||
account_creator.with_soft_here_isolines_limit(create_params[:soft_here_isolines_limit])
|
||||
end
|
||||
|
||||
if create_params[:soft_obs_snapshot_limit].present?
|
||||
account_creator.with_soft_obs_snapshot_limit(create_params[:soft_obs_snapshot_limit])
|
||||
end
|
||||
|
||||
if create_params[:soft_obs_general_limit].present?
|
||||
account_creator.with_soft_obs_general_limit(create_params[:soft_obs_general_limit])
|
||||
end
|
||||
|
||||
if create_params[:soft_twitter_datasource_limit].present?
|
||||
account_creator.with_soft_twitter_datasource_limit(create_params[:soft_twitter_datasource_limit])
|
||||
end
|
||||
|
||||
if create_params[:soft_mapzen_routing_limit].present?
|
||||
account_creator.with_soft_mapzen_routing_limit(create_params[:soft_mapzen_routing_limit])
|
||||
end
|
||||
|
||||
if create_params[:force_password_change] == true
|
||||
account_creator.with_force_password_change
|
||||
end
|
||||
|
||||
unless account_creator.valid_creation?(current_viewer)
|
||||
render_jsonp(account_creator.validation_errors.full_messages, 410)
|
||||
return
|
||||
end
|
||||
|
||||
account_creator.enqueue_creation(self)
|
||||
|
||||
presentation = Carto::Api::UserPresenter.new(account_creator.user,
|
||||
current_viewer: current_viewer)
|
||||
.to_eumapi_poro
|
||||
|
||||
render_jsonp presentation, 200
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, user: account_creator.user.inspect)
|
||||
|
||||
render_jsonp('An error has ocurred. Please contact support', 500)
|
||||
end
|
||||
|
||||
def update
|
||||
render_jsonp('No update params provided', 410) && return if update_params.empty?
|
||||
|
||||
params_to_update = update_params
|
||||
|
||||
# ::User validation requires confirmation
|
||||
password = params_to_update[:password]
|
||||
params_to_update[:password_confirmation] = password
|
||||
|
||||
# NOTE: Verify soft limits BEFORE updating the user
|
||||
model_validation_ok = soft_limits_validation(@user, params_to_update)
|
||||
model_validation_ok &&= @user.valid_password?(:password, password, password) if password.present?
|
||||
@user.set_fields(params_to_update, params_to_update.keys)
|
||||
model_validation_ok &&= @user.valid_update?(current_user)
|
||||
|
||||
unless model_validation_ok
|
||||
render_jsonp(@user.errors.full_messages, 410)
|
||||
return
|
||||
end
|
||||
|
||||
@user.update_in_central
|
||||
@user.save
|
||||
|
||||
presentation = Carto::Api::UserPresenter.new(@user, current_viewer: current_viewer)
|
||||
.to_eumapi_poro
|
||||
|
||||
render_jsonp presentation, 200
|
||||
rescue CartoDB::CentralCommunicationFailure => e
|
||||
CartoDB.notify_exception(e)
|
||||
|
||||
render_jsonp 'Central comunication failure', 500
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @organization.owner_id == @user.id
|
||||
render_jsonp("Can't delete org owner", 401) and return
|
||||
end
|
||||
|
||||
force_destroy = params[:force].present?
|
||||
|
||||
if !force_destroy && @user.has_shared_entities?
|
||||
error_message = "Can't delete @user. 'Has shared entities"
|
||||
render_jsonp(error_message, 410 ) and return
|
||||
end
|
||||
|
||||
@user.set_force_destroy if force_destroy
|
||||
@user.destroy
|
||||
@user.delete_in_central
|
||||
|
||||
render_jsonp 'User deleted', 200
|
||||
rescue CartoDB::CentralCommunicationFailure => e
|
||||
CartoDB::Logger.error(exception: e, message: 'Central error deleting user from EUMAPI', user: @user)
|
||||
render_jsonp "User couldn't be deleted", 500
|
||||
rescue => e
|
||||
render_jsonp "User couldn't be deleted: #{e.message}", 500
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
COMMON_MUTABLE_ATTRIBUTES = [
|
||||
:email,
|
||||
:password,
|
||||
:quota_in_bytes,
|
||||
:soft_geocoding_limit,
|
||||
:soft_here_isolines_limit,
|
||||
:soft_obs_general_limit,
|
||||
:soft_obs_snapshot_limit,
|
||||
:soft_twitter_datasource_limit,
|
||||
:soft_mapzen_routing_limit,
|
||||
:viewer,
|
||||
:org_admin
|
||||
].freeze
|
||||
|
||||
# TODO: Use native strong params when in Rails 4+
|
||||
def create_params
|
||||
@create_params ||=
|
||||
permit(COMMON_MUTABLE_ATTRIBUTES + [:username, :force_password_change])
|
||||
end
|
||||
|
||||
# TODO: Use native strong params when in Rails 4+
|
||||
def update_params
|
||||
@update_params ||= permit(COMMON_MUTABLE_ATTRIBUTES)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
require_relative './user_presenter'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class OrganizationsController < ::Api::ApplicationController
|
||||
include OrganizationsHelper
|
||||
include PagedSearcher
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :users
|
||||
|
||||
before_filter :load_organization, :load_group
|
||||
|
||||
rescue_from Carto::ParamInvalidError, with: :rescue_from_carto_error
|
||||
|
||||
VALID_ORDER_PARAMS = [:username, :updated_at].freeze
|
||||
|
||||
def users
|
||||
page, per_page, order, = page_per_page_order_params(VALID_ORDER_PARAMS, default_per_page: 50,
|
||||
default_order: :username)
|
||||
query = params[:q]
|
||||
users_query = [@group, @organization].compact.first.users
|
||||
users_query = users_query.where('(username like ? or email like ?)', "%#{query}%", "#{query}") if query
|
||||
|
||||
total_user_entries = users_query.count
|
||||
users_query = users_query.offset(( page - 1 ) * per_page ).limit(per_page).order(order)
|
||||
users = users_query.all
|
||||
|
||||
render_jsonp({ users: users.map { |u|
|
||||
Carto::Api::UserPresenter.new(u, current_viewer: current_user, fetch_db_size: false).to_poro
|
||||
}, total_user_entries: total_user_entries, total_entries: users.count })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
module Carto
|
||||
module Api
|
||||
class OverlayPresenter
|
||||
|
||||
PUBLIC_VALUES = [:id, :order, :type, :template, :options, :visualization_id].freeze
|
||||
VIZJSON_VALUES = [:type, :order, :options, :template].freeze
|
||||
|
||||
def initialize(overlay)
|
||||
@overlay = overlay
|
||||
end
|
||||
|
||||
def to_poro
|
||||
values(@overlay, PUBLIC_VALUES)
|
||||
end
|
||||
|
||||
def to_vizjson_poro
|
||||
values(@overlay, VIZJSON_VALUES)
|
||||
end
|
||||
|
||||
alias_method :to_vizjson, :to_vizjson_poro
|
||||
|
||||
protected
|
||||
|
||||
def values(overlay, attribute_list)
|
||||
Hash[attribute_list.map { |attribute| [attribute, overlay.send(attribute)] }]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
require_dependency 'carto/controller_helper'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class OverlaysController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :index, :show, :create, :update, :destroy
|
||||
|
||||
before_filter :logged_users_only
|
||||
before_filter :load_visualization
|
||||
before_filter :check_current_user_has_permissions_on_vis
|
||||
before_filter :load_overlay, only: [:show, :update, :destroy]
|
||||
|
||||
rescue_from StandardError, with: :rescue_from_standard_error
|
||||
rescue_from Carto::CartoError, with: :rescue_from_carto_error
|
||||
|
||||
def index
|
||||
collection = @visualization.overlays.map do |overlay|
|
||||
Carto::Api::OverlayPresenter.new(overlay).to_poro
|
||||
end
|
||||
render_jsonp(collection)
|
||||
end
|
||||
|
||||
def show
|
||||
render_jsonp(Carto::Api::OverlayPresenter.new(@overlay).to_poro)
|
||||
end
|
||||
|
||||
def create
|
||||
@stats_aggregator.timing('overlays.create') do
|
||||
begin
|
||||
overlay = Carto::Overlay.new(type: params[:type],
|
||||
options: params[:options],
|
||||
template: params[:template],
|
||||
order: params[:order],
|
||||
visualization_id: @visualization.id)
|
||||
|
||||
saved = @stats_aggregator.timing('save') do
|
||||
overlay.save
|
||||
end
|
||||
if saved
|
||||
render_jsonp(Carto::Api::OverlayPresenter.new(overlay).to_poro)
|
||||
else
|
||||
render_jsonp({ errors: overlay.errors }, :unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@stats_aggregator.timing('overlays.update') do
|
||||
begin
|
||||
@overlay.type = params[:type] if params[:type]
|
||||
@overlay.options = params[:options] if params[:options]
|
||||
@overlay.template = params[:template] if params[:template]
|
||||
@overlay.order = params[:order] if params[:order]
|
||||
|
||||
saved = @stats_aggregator.timing('save') do
|
||||
@overlay.save
|
||||
end
|
||||
if saved
|
||||
render_jsonp(Carto::Api::OverlayPresenter.new(@overlay).to_poro)
|
||||
else
|
||||
render_jsonp({ errors: @overlay.errors }, :unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@stats_aggregator.timing('overlays.destroy') do
|
||||
@stats_aggregator.timing('delete') do
|
||||
@overlay.destroy
|
||||
end
|
||||
head :no_content
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def logged_users_only
|
||||
raise Carto::UnauthorizedError.new if current_user.nil?
|
||||
end
|
||||
|
||||
def load_visualization
|
||||
visualization_id = uuid_parameter('visualization_id')
|
||||
@visualization = Carto::Visualization.where(id: visualization_id).first
|
||||
raise Carto::LoadError.new("Visualization not found: #{visualization_id}") unless @visualization
|
||||
end
|
||||
|
||||
def check_current_user_has_permissions_on_vis
|
||||
unless @visualization.writable_by?(current_user)
|
||||
raise Carto::UnauthorizedError.new("#{current_user.id} doesn't own visualization #{@visualization.id}")
|
||||
end
|
||||
end
|
||||
|
||||
def load_overlay
|
||||
overlay_id = uuid_parameter('id')
|
||||
@overlay = @visualization.overlays.where(id: overlay_id).first
|
||||
raise Carto::LoadError.new("Overlay not found: #{overlay_id}") unless @overlay
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
module Carto
|
||||
module Api
|
||||
module PagedSearcher
|
||||
|
||||
VALID_ORDER_DIRECTIONS = [:asc, :desc].freeze
|
||||
|
||||
def page_per_page_params(default_per_page: 20)
|
||||
page = (params[:page].presence || 1).to_i
|
||||
unless page > 0
|
||||
raise Carto::ParamInvalidError.new('page')
|
||||
end
|
||||
per_page = (params[:per_page].presence || default_per_page).to_i
|
||||
unless per_page > 0
|
||||
raise Carto::ParamInvalidError.new('per_page')
|
||||
end
|
||||
[page, per_page]
|
||||
end
|
||||
|
||||
def page_per_page_order_params(valid_order_values, default_per_page: 20, default_order: 'updated_at',
|
||||
default_order_direction: 'desc', valid_order_combinations: [])
|
||||
page, per_page = page_per_page_params(default_per_page: default_per_page)
|
||||
|
||||
order = extract_param(name: :order, default_value: default_order, valid_values: valid_order_values,
|
||||
valid_combinations: valid_order_combinations)
|
||||
order_direction = extract_param(name: :order_direction, default_value: default_order_direction,
|
||||
valid_values: VALID_ORDER_DIRECTIONS,
|
||||
valid_combinations: VALID_ORDER_DIRECTIONS)
|
||||
|
||||
[page, per_page, order, order_direction]
|
||||
end
|
||||
|
||||
def paged_result(result:, total_count:, page:, per_page:, params:)
|
||||
last_page = (total_count / per_page.to_f).ceil
|
||||
link_params = params.merge(per_page: per_page)
|
||||
|
||||
metadata = {
|
||||
total: total_count,
|
||||
count: result.count,
|
||||
result: result,
|
||||
_links: {
|
||||
first: { href: yield(link_params.merge(page: 1)) },
|
||||
last: { href: yield(link_params.merge(page: last_page)) }
|
||||
}
|
||||
}
|
||||
|
||||
metadata[:_links][:prev] = { href: yield(link_params.merge(page: page - 1)) } if page > 1
|
||||
metadata[:_links][:next] = { href: yield(link_params.merge(page: page + 1)) } if last_page > page
|
||||
metadata
|
||||
end
|
||||
|
||||
def extract_param(name:, default_value:, valid_values:, valid_combinations:)
|
||||
param = (params[name].presence || default_value).to_sym
|
||||
param_values = param.to_s.split(',').map(&:to_sym)
|
||||
single_parameter = valid_combinations.empty? || param_values.size == 1
|
||||
|
||||
if single_parameter && valid_values.exclude?(param)
|
||||
raise Carto::ParamInvalidError.new(name, valid_values)
|
||||
end
|
||||
|
||||
if !single_parameter && (param_values - valid_combinations).present?
|
||||
raise Carto::ParamCombinationInvalidError.new(name, valid_combinations)
|
||||
end
|
||||
|
||||
param
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,107 @@
|
||||
require_relative 'user_presenter'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class PermissionPresenter
|
||||
|
||||
def initialize(permission, current_viewer: nil, fetch_user_groups: false)
|
||||
@permission = permission
|
||||
@presenter_cache = Carto::Api::PresenterCache.new
|
||||
@current_viewer = current_viewer
|
||||
@fetch_user_groups = fetch_user_groups
|
||||
end
|
||||
|
||||
def with_presenter_cache(presenter_cache)
|
||||
@presenter_cache = presenter_cache
|
||||
self
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return to_public_poro unless current_viewer && @permission.user_has_read_permission?(current_viewer)
|
||||
|
||||
owner = @presenter_cache.get_poro(@permission.owner) do
|
||||
Carto::Api::UserPresenter.new(@permission.owner,
|
||||
fetch_groups: fetch_user_groups,
|
||||
current_viewer: current_viewer,
|
||||
fetch_profile: false)
|
||||
end
|
||||
|
||||
{
|
||||
id: @permission.id,
|
||||
owner: owner,
|
||||
entity: {
|
||||
id: @permission.visualization.id,
|
||||
type: 'vis'
|
||||
},
|
||||
acl: @permission.acl.map { |entry|
|
||||
entity = entity_decoration(entry)
|
||||
if entity.blank?
|
||||
nil
|
||||
else
|
||||
{
|
||||
type: entry[:type],
|
||||
entity: entity,
|
||||
access: entry[:access]
|
||||
}
|
||||
end
|
||||
}.reject(&:nil?),
|
||||
created_at: @permission.created_at,
|
||||
updated_at: @permission.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
def to_public_poro
|
||||
owner = @presenter_cache.get_poro(@permission.owner) do
|
||||
Carto::Api::UserPresenter.new(@permission.owner,
|
||||
fetch_groups: fetch_user_groups, current_viewer: current_viewer)
|
||||
end
|
||||
|
||||
{
|
||||
id: @permission.id,
|
||||
owner: owner
|
||||
}
|
||||
end
|
||||
|
||||
def entity_decoration(entry)
|
||||
if entry[:type] == Carto::Permission::TYPE_USER
|
||||
user_decoration(entry[:id])
|
||||
elsif entry[:type] == Carto::Permission::TYPE_ORGANIZATION
|
||||
organization_decoration(entry[:id])
|
||||
elsif entry[:type] == Carto::Permission::TYPE_GROUP
|
||||
group_decoration(entry[:id])
|
||||
else
|
||||
raise "Unknown entity type: #{entry[:type]}"
|
||||
end
|
||||
end
|
||||
|
||||
def user_decoration(user_id)
|
||||
user = ::User.where(id: user_id).first
|
||||
return {} if user.nil?
|
||||
Carto::Api::UserPresenter.new(user, fetch_groups: fetch_user_groups).to_public_poro
|
||||
end
|
||||
|
||||
def organization_decoration(org_id)
|
||||
org = Carto::Organization.where(id: org_id).first
|
||||
return {} if org.nil?
|
||||
{
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
avatar_url: org.avatar_url
|
||||
}
|
||||
end
|
||||
|
||||
def group_decoration(group_id)
|
||||
group = Carto::Group.where(id: group_id).first
|
||||
return {} if group.nil?
|
||||
{
|
||||
id: group.id,
|
||||
name: group.name
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_viewer, :fetch_user_groups
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
class Carto::Api::PermissionsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
extend Carto::DefaultRescueFroms
|
||||
|
||||
ssl_required :update
|
||||
|
||||
def update
|
||||
permission = Carto::Permission.where(id: params[:id]).first
|
||||
|
||||
return head(404) if permission.nil?
|
||||
return head(401) unless permission.is_owner?(current_user)
|
||||
|
||||
begin
|
||||
acl = params[:acl]
|
||||
acl ||= []
|
||||
permission.acl = acl.map(&:deep_symbolize_keys)
|
||||
rescue CartoDB::PermissionError => e
|
||||
CartoDB::Logger.error(exception: e)
|
||||
return head(400)
|
||||
end
|
||||
|
||||
permission.save!
|
||||
|
||||
render json: Carto::Api::PermissionPresenter.new(permission,
|
||||
current_viewer: current_viewer, fetch_user_groups: true).to_poro
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
module Carto
|
||||
module Api
|
||||
class PresenterCache
|
||||
|
||||
def initialize
|
||||
@cache = Hash.new
|
||||
end
|
||||
|
||||
# Caches to_poro of the presenter that is passed in a block, based on model class and id. Example:
|
||||
# cache.get_poro(user) { Carto::Api::UserPresenter.new(user, { fetch_groups: false } ) }
|
||||
def get_poro(model)
|
||||
raise "no model given" unless model
|
||||
|
||||
model_class = model.class
|
||||
model_id = model.id
|
||||
|
||||
if model_id.nil?
|
||||
presenter = yield
|
||||
raise "no presenter given" if presenter.nil?
|
||||
return presenter.to_poro
|
||||
end
|
||||
|
||||
class_cache = get_class_cache(model_class)
|
||||
|
||||
unless class_cache[model_id]
|
||||
presenter = yield
|
||||
raise "no presenter given" if presenter.nil?
|
||||
class_cache[model_id] = presenter.to_poro
|
||||
end
|
||||
|
||||
class_cache[model_id]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_class_cache(model_class)
|
||||
unless @cache[model_class]
|
||||
@cache[model_class] = Hash.new
|
||||
end
|
||||
@cache[model_class]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
class Carto::Api::Public::ApplicationController < ::Api::ApplicationController
|
||||
include Carto::Api::AuthApiAuthentication
|
||||
|
||||
# Override all CORS settings
|
||||
skip_before_action :cors_preflight_check
|
||||
skip_after_action :allow_cross_domain_access
|
||||
before_action :allow_full_cross_domain_access
|
||||
|
||||
# Only allow API Key authorization
|
||||
skip_before_action :http_header_authentication, :api_authorization_required
|
||||
prepend_before_action :only_api_key_authorization
|
||||
|
||||
# Disable authorization check for OPTIONS
|
||||
skip_before_action :only_api_key_authorization, only: [:options]
|
||||
|
||||
WARDEN_SCOPE = :$public_api_scope
|
||||
|
||||
def allow_full_cross_domain_access
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = '*'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Authorization'
|
||||
end
|
||||
|
||||
def options
|
||||
head :ok
|
||||
end
|
||||
|
||||
# Override current_user and current_viewer so they only return the user authenticated via API Key (ignore session)
|
||||
def current_user
|
||||
warden.user(WARDEN_SCOPE)
|
||||
end
|
||||
|
||||
def current_viewer
|
||||
current_user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def only_api_key_authorization
|
||||
authenticate!(:any_auth_api, :api_authentication, scope: WARDEN_SCOPE)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,144 @@
|
||||
require_relative '../visualization_searcher'
|
||||
require_relative '../paged_searcher'
|
||||
|
||||
class Carto::Api::Public::CustomVisualizationsController < Carto::Api::Public::ApplicationController
|
||||
include Carto::Api::VisualizationSearcher
|
||||
include Carto::Api::PagedSearcher
|
||||
|
||||
CONTENT_LENGTH_LIMIT_IN_BYTES = 100000
|
||||
VALID_ORDER_PARAMS = %i(name updated_at privacy).freeze
|
||||
ALLOWED_PRIVACY_MODES = [
|
||||
Carto::Visualization::PRIVACY_PUBLIC,
|
||||
Carto::Visualization::PRIVACY_PROTECTED
|
||||
].freeze
|
||||
|
||||
ssl_required
|
||||
|
||||
before_action :validate_mandatory_creation_params, only: [:create]
|
||||
before_action :validate_input_parameters, only: [:create, :update]
|
||||
before_action :get_kuviz, only: [:update, :delete]
|
||||
before_action :get_user, only: [:create, :update, :delete]
|
||||
before_action :check_for_permission, only: [:update, :delete]
|
||||
|
||||
def index
|
||||
opts = { valid_order_combinations: VALID_ORDER_PARAMS }
|
||||
page, per_page, order, order_direction = page_per_page_order_params(VALID_ORDER_PARAMS, opts)
|
||||
params[:type] = Carto::Visualization::TYPE_KUVIZ
|
||||
vqb = query_builder_with_filter_from_hash(params)
|
||||
|
||||
visualizations = vqb.with_order(order, order_direction)
|
||||
.build_paged(page, per_page).map do |v|
|
||||
Carto::Api::Public::KuvizPresenter.new(self, v.user, v).to_hash
|
||||
end
|
||||
response = {
|
||||
visualizations: visualizations,
|
||||
total_entries: vqb.build.size
|
||||
}
|
||||
render_jsonp(response)
|
||||
rescue Carto::ParamInvalidError, Carto::ParamCombinationInvalidError => e
|
||||
render_jsonp({ error: e.message }, e.status)
|
||||
rescue StandardError => e
|
||||
CartoDB::Logger.error(exception: e)
|
||||
render_jsonp({ error: e.message }, 500)
|
||||
end
|
||||
|
||||
def create
|
||||
kuviz = create_visualization_metadata(@logged_user)
|
||||
asset = Carto::Asset.for_visualization(visualization: kuviz,
|
||||
resource: StringIO.new(Base64.decode64(params[:data])))
|
||||
asset.save
|
||||
|
||||
render_jsonp(Carto::Api::Public::KuvizPresenter.new(self, @logged_user, kuviz).to_hash, 200)
|
||||
rescue StandardError => e
|
||||
CartoDB::Logger.error(message: 'Error creating kuviz', params: params, exception: e)
|
||||
render_jsonp({ error: 'cant create the kuviz' }, 500)
|
||||
end
|
||||
|
||||
def update
|
||||
@kuviz.update_attributes!(params.permit(:name, :privacy, :password))
|
||||
|
||||
if params[:data].present?
|
||||
@kuviz.asset.update_visualization_resource(StringIO.new(Base64.decode64(params[:data])))
|
||||
# In case we only update the asset we need to invalidate the visualization
|
||||
@kuviz.save
|
||||
end
|
||||
render_jsonp(Carto::Api::Public::KuvizPresenter.new(self, @logged_user, @kuviz).to_hash, 200)
|
||||
end
|
||||
|
||||
def delete
|
||||
@kuviz.destroy
|
||||
head 204
|
||||
rescue StandardError => exception
|
||||
CartoDB::Logger.error(message: 'Error deleting kuviz', exception: exception,
|
||||
visualization: @kuviz)
|
||||
render_jsonp({ errors: [exception.message] }, 400)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_visualization_metadata(user)
|
||||
kuviz = Carto::Visualization.new
|
||||
kuviz.name = params[:name]
|
||||
kuviz.privacy = params[:password].present? ? Carto::Visualization::PRIVACY_PROTECTED : Carto::Visualization::PRIVACY_PUBLIC
|
||||
kuviz.password = params[:password]
|
||||
kuviz.type = Carto::Visualization::TYPE_KUVIZ
|
||||
kuviz.user = user
|
||||
kuviz.save
|
||||
kuviz
|
||||
end
|
||||
|
||||
def get_user
|
||||
@logged_user = current_viewer.present? ? Carto::User.find(current_viewer.id) : nil
|
||||
end
|
||||
|
||||
def check_for_permission
|
||||
head(403) unless @kuviz.has_permission?(@logged_user, Carto::Permission::ACCESS_READWRITE)
|
||||
end
|
||||
|
||||
def validate_input_parameters
|
||||
if request.content_length > CONTENT_LENGTH_LIMIT_IN_BYTES
|
||||
return render_jsonp({ error: "visualization over the size limit (#{CONTENT_LENGTH_LIMIT_IN_BYTES})" }, 400)
|
||||
end
|
||||
|
||||
if params[:privacy].present?
|
||||
unless ALLOWED_PRIVACY_MODES.include?(params[:privacy])
|
||||
return render_jsonp({ error: "privacy mode not allowed. Allowed ones are #{ALLOWED_PRIVACY_MODES}" }, 400)
|
||||
end
|
||||
if params[:privacy] == Carto::Visualization::PRIVACY_PROTECTED && !params[:password].present?
|
||||
return render_jsonp({ error: 'Changing privacy to protected should come along with the password param' }, 400)
|
||||
end
|
||||
end
|
||||
|
||||
if params[:data].present?
|
||||
begin
|
||||
decoded_data = Base64.strict_decode64(params[:data])
|
||||
return render_jsonp({ error: 'data parameter must be HTML' }, 400) unless html_param?(decoded_data)
|
||||
rescue ArgumentError
|
||||
return render_jsonp({ error: 'data parameter must be encoded in base64' }, 400)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate_mandatory_creation_params
|
||||
if !params[:data].present?
|
||||
render_jsonp({ error: 'missing data parameter' }, 400)
|
||||
elsif !params[:name].present?
|
||||
render_jsonp({ error: 'missing name parameter' }, 400)
|
||||
end
|
||||
end
|
||||
|
||||
def html_param?(data)
|
||||
# FIXME this is a very naive implementantion. I'm trying to use
|
||||
# Nokogiri to validate the HTML but it doesn't works as I want
|
||||
# so
|
||||
data.match(/\<html.*\>/).present?
|
||||
end
|
||||
|
||||
def get_kuviz
|
||||
@kuviz = Carto::Visualization.find(params[:id])
|
||||
if @kuviz.nil?
|
||||
raise Carto::LoadError.new('Kuviz doesn\'t exist', 404)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,158 @@
|
||||
module Carto
|
||||
module Api
|
||||
module Public
|
||||
class DataObservatoryController < Carto::Api::Public::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
include Carto::Api::PagedSearcher
|
||||
extend Carto::DefaultRescueFroms
|
||||
|
||||
ssl_required
|
||||
|
||||
before_action :load_user
|
||||
before_action :load_filters, only: [:subscriptions]
|
||||
before_action :load_id, only: [:subscription_info, :subscribe, :unsubscribe]
|
||||
before_action :load_type, only: [:subscription_info, :subscribe]
|
||||
before_action :check_api_key_permissions
|
||||
before_action :check_licensing_enabled, only: [:subscription_info, :subscribe, :unsubscribe]
|
||||
|
||||
setup_default_rescues
|
||||
|
||||
respond_to :json
|
||||
|
||||
BIGQUERY_KEY = 'bq'.freeze
|
||||
VALID_TYPES = %w(dataset geography).freeze
|
||||
DATASET_REGEX = /[\w\-]+\.[\w\-]+\.[\w\-]+/.freeze
|
||||
VALID_ORDER_PARAMS = %i(id table dataset project type).freeze
|
||||
METADATA_FIELDS = %i(id estimated_delivery_days subscription_list_price tos tos_link licenses licenses_link
|
||||
rights type).freeze
|
||||
TABLES_BY_TYPE = { 'dataset' => 'datasets', 'geography' => 'geographies' }.freeze
|
||||
|
||||
def token
|
||||
response = Cartodb::Central.new.get_do_token(@user.username)
|
||||
render(json: response)
|
||||
end
|
||||
|
||||
def subscriptions
|
||||
available_subscriptions = bq_subscriptions.select { |dataset| Time.parse(dataset['expires_at']) > Time.now }
|
||||
response = present_subscriptions(available_subscriptions)
|
||||
render(json: { subscriptions: response })
|
||||
end
|
||||
|
||||
def subscription_info
|
||||
response = present_metadata(subscription_metadata)
|
||||
|
||||
render(json: response)
|
||||
end
|
||||
|
||||
def subscribe
|
||||
metadata = subscription_metadata
|
||||
response = present_metadata(metadata)
|
||||
|
||||
return render(json: response) if metadata[:estimated_delivery_days].positive?
|
||||
|
||||
license_info = {
|
||||
dataset_id: metadata[:id],
|
||||
available_in: metadata[:available_in],
|
||||
price: metadata[:subscription_list_price],
|
||||
expires_at: Time.now.round + 1.year
|
||||
}
|
||||
Carto::DoLicensingService.new(@user.username).subscribe([license_info])
|
||||
|
||||
render(json: response)
|
||||
end
|
||||
|
||||
def unsubscribe
|
||||
Carto::DoLicensingService.new(@user.username).unsubscribe(@id)
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user
|
||||
@user = Carto::User.find(current_viewer.id)
|
||||
end
|
||||
|
||||
def load_filters
|
||||
_, _, @order, @direction = page_per_page_order_params(
|
||||
VALID_ORDER_PARAMS, default_order: 'id', default_order_direction: 'asc'
|
||||
)
|
||||
load_type(required: false)
|
||||
end
|
||||
|
||||
def load_id
|
||||
@id = params[:id]
|
||||
raise ParamInvalidError.new(:id) unless @id =~ DATASET_REGEX
|
||||
end
|
||||
|
||||
def load_type(required: true)
|
||||
@type = params[:type]
|
||||
return if @type.nil? && !required
|
||||
|
||||
raise ParamInvalidError.new(:type, VALID_TYPES.join(', ')) unless VALID_TYPES.include?(@type)
|
||||
end
|
||||
|
||||
def check_api_key_permissions
|
||||
api_key = Carto::ApiKey.find_by_token(params["api_key"])
|
||||
raise UnauthorizedError unless api_key&.master? || api_key&.data_observatory_permissions?
|
||||
end
|
||||
|
||||
def check_licensing_enabled
|
||||
raise UnauthorizedError.new('DO licensing not enabled') unless @user.has_feature_flag?('do-licensing')
|
||||
end
|
||||
|
||||
def rescue_from_central_error(exception)
|
||||
render_jsonp({ errors: exception.errors }, 500)
|
||||
end
|
||||
|
||||
def bq_subscriptions
|
||||
redis_key = "do:#{@user.username}:datasets"
|
||||
redis_value = $users_metadata.hget(redis_key, BIGQUERY_KEY) || '[]'
|
||||
JSON.parse(redis_value)
|
||||
end
|
||||
|
||||
def present_subscriptions(subscriptions)
|
||||
enriched_subscriptions = subscriptions.map do |subscription|
|
||||
qualified_id = subscription['dataset_id']
|
||||
project, dataset, table = qualified_id.split('.')
|
||||
# FIXME: better save the type in Redis or look for it in the metadata tables
|
||||
type = table.starts_with?('geography') ? 'geography' : 'dataset'
|
||||
{ project: project, dataset: dataset, table: table, id: qualified_id, type: type }
|
||||
end
|
||||
enriched_subscriptions.select! { |subscription| subscription[:type] == @type } if @type
|
||||
ordered_subscriptions = enriched_subscriptions.sort_by { |subscription| subscription[@order] }
|
||||
@direction == :asc ? ordered_subscriptions : ordered_subscriptions.reverse
|
||||
end
|
||||
|
||||
def present_metadata(metadata)
|
||||
metadata[:estimated_delivery_days] = metadata[:estimated_delivery_days].to_f
|
||||
metadata[:subscription_list_price] = metadata[:subscription_list_price].to_f
|
||||
metadata.slice(*METADATA_FIELDS)
|
||||
end
|
||||
|
||||
def subscription_metadata
|
||||
metadata_user = ::User.where(username: 'do-metadata').first
|
||||
raise Carto::LoadError.new('No Data Observatory metadata found') unless metadata_user
|
||||
|
||||
query = "SELECT *, '#{@type}' as type FROM #{TABLES_BY_TYPE[@type]} WHERE id = '#{@id}'"
|
||||
|
||||
result = metadata_user.in_database[query].first
|
||||
validate_metadata(result)
|
||||
end
|
||||
|
||||
def validate_metadata(result)
|
||||
raise Carto::LoadError.new("No metadata found for #{@id}") unless result
|
||||
|
||||
valid_data = result[:available_in].present? && result[:estimated_delivery_days].present? &&
|
||||
result[:subscription_list_price].present?
|
||||
unless valid_data
|
||||
CartoDB::Logger.info(message: 'Incomplete DO metadata', id: @id)
|
||||
raise Carto::LoadError.new("Incomplete metadata found for #{@id}")
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
require_relative '../paged_searcher'
|
||||
require_dependency 'carto/oauth_provider/errors'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
module Public
|
||||
class DatasetsController < Carto::Api::Public::ApplicationController
|
||||
include Carto::Api::PagedSearcher
|
||||
include Carto::ControllerHelper
|
||||
extend Carto::DefaultRescueFroms
|
||||
|
||||
ssl_required
|
||||
|
||||
before_action :load_user
|
||||
before_action :load_params
|
||||
before_action :check_permissions
|
||||
before_action :engine_required
|
||||
|
||||
setup_default_rescues
|
||||
rescue_from Carto::OauthProvider::Errors::ServerError, with: :rescue_oauth_errors
|
||||
|
||||
VALID_ORDER_PARAMS = %i(name type).freeze
|
||||
VALID_TYPE_PARAMS = %w(table view matview).freeze
|
||||
|
||||
def index
|
||||
tables = @user.in_database[select_tables_query].all
|
||||
result = enrich_tables(tables)
|
||||
total = @user.in_database[count_tables_query].first[:count]
|
||||
|
||||
render_paged(result, total)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user
|
||||
@user = ::User.where(id: current_viewer.id).first
|
||||
end
|
||||
|
||||
def load_params
|
||||
@page, @per_page, @order, @direction = page_per_page_order_params(
|
||||
VALID_ORDER_PARAMS, default_order: 'name', default_order_direction: 'asc'
|
||||
)
|
||||
@offset = (@page - 1) * @per_page
|
||||
@types = load_type
|
||||
end
|
||||
|
||||
def load_type
|
||||
types = params[:type]&.split(',').to_a
|
||||
raise Carto::ParamInvalidError.new(:type, VALID_TYPE_PARAMS) if (types - VALID_TYPE_PARAMS).any?
|
||||
|
||||
types.empty? ? VALID_TYPE_PARAMS : types
|
||||
end
|
||||
|
||||
def check_permissions
|
||||
api_key = Carto::ApiKey.find_by_token(params["api_key"])
|
||||
raise UnauthorizedError unless api_key.master? || api_key.dataset_metadata_permissions
|
||||
end
|
||||
|
||||
def enrich_tables(tables)
|
||||
table_names = tables.map { |table| table[:name] }
|
||||
visualizations = table_visualizations(table_names)
|
||||
tables.map do |table|
|
||||
visualizations.find { |visualization| visualization[:name] == table[:name] } || table
|
||||
end
|
||||
end
|
||||
|
||||
def table_visualizations(names)
|
||||
visualizations = Carto::VisualizationQueryBuilder.new
|
||||
.with_user_id(@user.id)
|
||||
.with_name(names)
|
||||
.with_type(Carto::Visualization::TYPE_CANONICAL)
|
||||
.build.all
|
||||
visualizations.map do |visualization|
|
||||
{
|
||||
name: visualization.name,
|
||||
cartodbfied: true,
|
||||
type: 'table',
|
||||
privacy: visualization.privacy,
|
||||
updated_at: visualization.updated_at
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def select_tables_query
|
||||
%{
|
||||
SELECT * FROM (#{tables_and_views_query}) AS tables_and_views
|
||||
ORDER BY #{@order} #{@direction}
|
||||
LIMIT #{@per_page}
|
||||
OFFSET #{@offset}
|
||||
}.squish
|
||||
end
|
||||
|
||||
def count_tables_query
|
||||
"SELECT COUNT(*) FROM (#{tables_and_views_query}) AS tables_and_views"
|
||||
end
|
||||
|
||||
def tables_and_views_query
|
||||
@types.map { |type|
|
||||
%{
|
||||
SELECT #{type}name AS name, '#{type}' AS type, false AS cartodbfied, NULL AS privacy, NULL AS updated_at
|
||||
FROM pg_#{type}s
|
||||
WHERE schemaname = '#{@user.database_schema}'
|
||||
AND #{type}owner <> 'postgres'
|
||||
}
|
||||
}.join(' UNION ').squish
|
||||
end
|
||||
|
||||
def render_paged(result, total)
|
||||
enriched_response = paged_result(
|
||||
result: result,
|
||||
total_count: total,
|
||||
page: @page,
|
||||
per_page: @per_page,
|
||||
params: params.except('controller', 'action')
|
||||
) { |params| api_v4_datasets_url(params) }
|
||||
|
||||
render_jsonp(enriched_response, 200)
|
||||
end
|
||||
|
||||
def rescue_oauth_errors(exception)
|
||||
render json: { errors: exception.parameters[:error_description] }, status: 500
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
module Carto
|
||||
module Api
|
||||
module Public
|
||||
class KuvizPresenter
|
||||
|
||||
def initialize(context, user, kuviz)
|
||||
@context = context
|
||||
@user = user
|
||||
@kuviz = kuviz
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
id: @kuviz.id,
|
||||
name: @kuviz.name,
|
||||
privacy: @kuviz.privacy,
|
||||
created_at: @kuviz.created_at,
|
||||
updated_at: @kuviz.updated_at,
|
||||
url: CartoDB.url(@context, 'kuviz_show',
|
||||
params: { id: @kuviz.id },
|
||||
user: @user)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
module Carto
|
||||
module Api
|
||||
module Public
|
||||
class OauthAppPresenter
|
||||
|
||||
PRIVATE_ATTRIBUTES = %i(
|
||||
id user_id name created_at updated_at client_id client_secret
|
||||
redirect_uris icon_url restricted description website_url
|
||||
).freeze
|
||||
|
||||
PUBLIC_ATTRIBUTES = %i(id name created_at updated_at description website_url icon_url).freeze
|
||||
|
||||
def initialize(oauth_app, user: nil)
|
||||
@oauth_app = oauth_app
|
||||
@user = user
|
||||
end
|
||||
|
||||
def to_hash(private_data: false)
|
||||
private_data ? to_private_hash : to_public_hash
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_private_hash
|
||||
@oauth_app.slice(*PRIVATE_ATTRIBUTES).merge(username: @oauth_app.user.username)
|
||||
end
|
||||
|
||||
def to_public_hash
|
||||
oauth_app_user = @oauth_app.oauth_app_users.where(user: @user).first
|
||||
scopes = Carto::OauthProvider::Scopes.scopes_by_category(oauth_app_user&.all_scopes)
|
||||
@oauth_app.slice(*PUBLIC_ATTRIBUTES).merge(scopes: scopes)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,143 @@
|
||||
require_relative '../paged_searcher'
|
||||
require_dependency 'carto/oauth_provider/errors'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
module Public
|
||||
class OauthAppsController < Carto::Api::Public::ApplicationController
|
||||
include Carto::Api::PagedSearcher
|
||||
include Carto::ControllerHelper
|
||||
extend Carto::DefaultRescueFroms
|
||||
|
||||
ssl_required
|
||||
|
||||
before_action :load_user
|
||||
before_action :load_owned_app, only: [:show, :update, :regenerate_secret, :destroy]
|
||||
before_action :load_granted_app, only: :revoke
|
||||
before_action :load_index_params, only: [:index, :index_granted]
|
||||
before_action :engine_required
|
||||
|
||||
setup_default_rescues
|
||||
rescue_from Carto::OauthProvider::Errors::ServerError, with: :rescue_oauth_errors
|
||||
|
||||
VALID_ORDER_PARAMS = [:name, :updated_at, :restricted, :user_id].freeze
|
||||
|
||||
def index
|
||||
oauth_apps = user_or_organization_apps
|
||||
render_paged(oauth_apps, private_data: true) { |params| api_v4_oauth_apps_url(params) }
|
||||
end
|
||||
|
||||
def index_granted
|
||||
oauth_apps = @user.granted_oauth_apps
|
||||
render_paged(oauth_apps) { |params| api_v4_oauth_apps_index_granted_url(params) }
|
||||
end
|
||||
|
||||
def show
|
||||
render_jsonp(OauthAppPresenter.new(@oauth_app).to_hash(private_data: true), 200)
|
||||
end
|
||||
|
||||
def create
|
||||
create_params = permitted_params.merge(user: @user)
|
||||
@oauth_app = OauthApp.create!(create_params)
|
||||
track_event('CreatedOauthApp')
|
||||
render_jsonp(OauthAppPresenter.new(@oauth_app).to_hash(private_data: true), 201)
|
||||
end
|
||||
|
||||
def update
|
||||
@oauth_app.update_attributes!(permitted_params)
|
||||
render_jsonp(OauthAppPresenter.new(@oauth_app).to_hash(private_data: true), 200)
|
||||
end
|
||||
|
||||
def regenerate_secret
|
||||
@oauth_app.regenerate_client_secret!
|
||||
render_jsonp(OauthAppPresenter.new(@oauth_app.reload).to_hash(private_data: true), 200)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@oauth_app.destroy!
|
||||
track_event('DeletedOauthApp')
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def revoke
|
||||
oauth_app_user = @oauth_app.oauth_app_users.where(user_id: @user.id).first
|
||||
oauth_app_user.destroy!
|
||||
track_event('DeletedOauthAppUser')
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user
|
||||
@user = Carto::User.find(current_viewer.id)
|
||||
end
|
||||
|
||||
def load_owned_app
|
||||
@oauth_app = Carto::OauthApp.find(params[:id])
|
||||
raise ActiveRecord::RecordNotFound.new unless owned?
|
||||
end
|
||||
|
||||
def load_granted_app
|
||||
@oauth_app = Carto::OauthApp.find(params[:id])
|
||||
raise ActiveRecord::RecordNotFound.new unless granted?
|
||||
end
|
||||
|
||||
def owned?
|
||||
return true if @oauth_app.user_id == @user.id
|
||||
|
||||
@user.organization_admin? && @user.organization == @oauth_app.user.organization
|
||||
end
|
||||
|
||||
def granted?
|
||||
@user.granted_oauth_apps.include?(@oauth_app)
|
||||
end
|
||||
|
||||
def load_index_params
|
||||
@page, @per_page, @order = page_per_page_order_params(VALID_ORDER_PARAMS)
|
||||
end
|
||||
|
||||
def user_or_organization_apps
|
||||
return @user.oauth_apps unless @user.organization_admin?
|
||||
|
||||
org_users = @user.organization.users
|
||||
Carto::OauthApp.where(user: org_users)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:name, :icon_url, :description, :website_url, redirect_uris: [])
|
||||
end
|
||||
|
||||
def render_paged(oauth_apps, private_data: false)
|
||||
filtered_oauth_apps = Carto::PagedModel.paged_association(oauth_apps, @page, @per_page, @order)
|
||||
result = filtered_oauth_apps.map do |oauth_app|
|
||||
OauthAppPresenter.new(oauth_app, user: @user).to_hash(private_data: private_data)
|
||||
end
|
||||
|
||||
enriched_response = paged_result(
|
||||
result: result,
|
||||
total_count: oauth_apps.size,
|
||||
page: @page,
|
||||
per_page: @per_page,
|
||||
params: params.except('controller', 'action')
|
||||
) { |params| yield(params) }
|
||||
|
||||
render_jsonp(enriched_response, 200)
|
||||
end
|
||||
|
||||
def rescue_oauth_errors(exception)
|
||||
render json: { errors: exception.parameters[:error_description] }, status: 500
|
||||
end
|
||||
|
||||
def track_event(event_name)
|
||||
properties = {
|
||||
user_id: @user.id,
|
||||
app_id: @oauth_app.id,
|
||||
app_name: @oauth_app.name
|
||||
}
|
||||
event_class = "Carto::Tracking::Events::#{event_name}".constantize
|
||||
event_class.new(@user.id, properties).report
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
module Carto
|
||||
module Api
|
||||
module Public
|
||||
class OrganizationPublicProfilePresenter
|
||||
def initialize(organization)
|
||||
@organization = organization
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
owner: {
|
||||
username: @organization.owner.username
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
module Carto
|
||||
module Api
|
||||
module Public
|
||||
class UserPublicPresenter
|
||||
include SqlApiHelper
|
||||
include MapsApiHelper
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def to_hash
|
||||
base_rails_url = CartoDB.base_url(@user.username)
|
||||
|
||||
{
|
||||
username: @user.username,
|
||||
organization: @user.has_organization? ? OrganizationPublicPresenter.new(@user.organization).to_hash : nil,
|
||||
api_endpoints: {
|
||||
sql: sql_api_url(@user.username),
|
||||
maps: maps_api_url(@user.username),
|
||||
import: base_rails_url,
|
||||
auth: base_rails_url
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
module Carto
|
||||
module Api
|
||||
module Public
|
||||
class UserPublicProfilePresenter < UserPublicPresenter
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def to_hash
|
||||
org_hash = OrganizationPublicProfilePresenter.new(@user.organization).to_hash if @user.has_organization?
|
||||
|
||||
super.deep_merge(
|
||||
organization: org_hash,
|
||||
avatar_url: @user.avatar_url,
|
||||
first_name: @user.name,
|
||||
last_name: @user.last_name
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
module Carto
|
||||
module Api
|
||||
module Public
|
||||
class UsersController < Carto::Api::Public::ApplicationController
|
||||
ssl_required
|
||||
|
||||
def me_public
|
||||
presentation = if request_api_key.user_data.try(:include?, 'profile')
|
||||
UserPublicProfilePresenter.new(request_api_key.user).to_hash
|
||||
else
|
||||
UserPublicPresenter.new(request_api_key.user).to_hash
|
||||
end
|
||||
|
||||
render(json: presentation)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
class Carto::Api::ReceivedNotificationPresenter
|
||||
include Carto::HtmlSafe
|
||||
|
||||
def initialize(received_notification)
|
||||
@received_notification = received_notification
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
id: @received_notification.id,
|
||||
icon: @received_notification.icon,
|
||||
html_body: markdown_html_safe(@received_notification.body),
|
||||
received_at: @received_notification.received_at,
|
||||
read_at: @received_notification.read_at
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
module Carto
|
||||
module Api
|
||||
class ReceivedNotificationsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
extend Carto::DefaultRescueFroms
|
||||
|
||||
ssl_required :update
|
||||
|
||||
before_filter :load_user
|
||||
before_filter :load_notification, only: [:update]
|
||||
|
||||
setup_default_rescues
|
||||
|
||||
def update
|
||||
changed_notification = params[:notification]
|
||||
if changed_notification
|
||||
read_at = changed_notification[:read_at]
|
||||
@received_notification.read_at = DateTime.parse(read_at) if read_at
|
||||
@received_notification.save!
|
||||
end
|
||||
|
||||
render_jsonp ReceivedNotificationPresenter.new(@received_notification).to_hash
|
||||
|
||||
rescue ArgumentError => e
|
||||
render_jsonp({ errors: { read_at: 'invalid date format' } }, 422)
|
||||
CartoDB::Logger.warning(exception: e)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user
|
||||
@user = Carto::User.find(current_user.id)
|
||||
raise Carto::UnauthorizedError.new('Can only access own notifications') unless @user.id == params[:user_id]
|
||||
end
|
||||
|
||||
def load_notification
|
||||
@received_notification = @user.received_notifications.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
require_relative '../../../models/carto/permission'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class RecordsController < ::Api::ApplicationController
|
||||
ssl_required :show, :create, :update, :destroy
|
||||
|
||||
REJECT_PARAMS = %w{ format controller action row_id requestId column_id
|
||||
api_key table_id oauth_token oauth_token_secret api_key user_domain }.freeze
|
||||
|
||||
before_filter :set_start_time
|
||||
before_filter :load_user_table, only: [:show, :create, :update, :destroy]
|
||||
before_filter :read_privileges?, only: [:show]
|
||||
before_filter :write_privileges?, only: [:create, :update, :destroy]
|
||||
|
||||
# This endpoint is not used by the editor but by users. Do not remove
|
||||
def show
|
||||
render_jsonp(@user_table.service.record(params[:id]))
|
||||
rescue => e
|
||||
CartoDB::Logger.error(message: 'Error loading record', exception: e,
|
||||
record_id: params[:id], user_table: @user_table)
|
||||
render_jsonp({ errors: ["Record #{params[:id]} not found"] }, 404)
|
||||
end
|
||||
|
||||
def create
|
||||
primary_key = @user_table.service.insert_row!(filtered_row)
|
||||
render_jsonp(@user_table.service.record(primary_key))
|
||||
rescue => e
|
||||
render_jsonp({ errors: [e.message] }, 400)
|
||||
end
|
||||
|
||||
def update
|
||||
if params[:cartodb_id].present?
|
||||
begin
|
||||
resp = @user_table.service.update_row!(params[:cartodb_id], filtered_row)
|
||||
|
||||
if resp > 0
|
||||
render_jsonp(@user_table.service.record(params[:cartodb_id]))
|
||||
else
|
||||
render_jsonp({ errors: ["row identified with #{params[:cartodb_id]} not found"] }, 404)
|
||||
end
|
||||
rescue => e
|
||||
CartoDB::Logger.warning(message: 'Error updating record', exception: e)
|
||||
render_jsonp({ errors: [translate_error(e.message.split("\n").first)] }, 400)
|
||||
end
|
||||
else
|
||||
render_jsonp({ errors: ["cartodb_id can't be blank"] }, 404)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
id = (params[:cartodb_id] =~ /\A\d+\z/ ? params[:cartodb_id] : params[:cartodb_id].to_s.split(','))
|
||||
schema_name = current_user.database_schema
|
||||
if current_user.id != @user_table.service.owner.id
|
||||
schema_name = @user_table.service.owner.database_schema
|
||||
end
|
||||
|
||||
current_user.in_database
|
||||
.select
|
||||
.from(Sequel.qualify(schema_name.to_sym, @user_table.service.name.to_sym))
|
||||
.where(cartodb_id: id)
|
||||
.delete
|
||||
|
||||
head :no_content
|
||||
rescue
|
||||
render_jsonp({ errors: ["row identified with #{params[:cartodb_id]} not found"] }, 404)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def filtered_row
|
||||
params.reject { |k, _| REJECT_PARAMS.include?(k) }.symbolize_keys
|
||||
end
|
||||
|
||||
def load_user_table
|
||||
@user_table = Carto::Helpers::TableLocator.new.get_by_id_or_name(params[:table_id], current_user)
|
||||
raise RecordNotFound unless @user_table
|
||||
end
|
||||
|
||||
def read_privileges?
|
||||
head(401) unless current_user && @user_table.visualization.is_viewable_by_user?(current_user)
|
||||
end
|
||||
|
||||
def write_privileges?
|
||||
head(401) unless current_user && @user_table.visualization.writable_by?(current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
require_dependency 'carto/controller_helper'
|
||||
|
||||
class Carto::Api::SearchPreviewController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required
|
||||
|
||||
before_filter :login_required
|
||||
before_filter :load_parameters
|
||||
|
||||
rescue_from StandardError, with: :rescue_from_standard_error
|
||||
rescue_from Carto::ParamCombinationInvalidError, with: :rescue_from_carto_error
|
||||
|
||||
DEFAULT_LIMIT = 4
|
||||
DEFAULT_TYPES = "derived,table,remote,tag".freeze
|
||||
VALID_TYPES = Carto::Visualization::VALID_TYPES + ["tag"]
|
||||
|
||||
def index
|
||||
searcher_params = { user: current_viewer, pattern: @pattern, types: @types, limit: @limit }
|
||||
searcher = Carto::DashboardPreviewSearcher.new(searcher_params)
|
||||
result = searcher.search
|
||||
|
||||
presenter_params = { dashboard_search_result: result, limit: @limit, current_viewer: current_viewer, context: self }
|
||||
presentation = Carto::Api::SearchPreviewPresenter.new(presenter_params).to_poro
|
||||
|
||||
render json: presentation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_parameters
|
||||
@limit = params.fetch(:limit, DEFAULT_LIMIT).to_i
|
||||
@pattern = params[:q]
|
||||
|
||||
@types = params.fetch(:types, DEFAULT_TYPES).split(',')
|
||||
if (@types - VALID_TYPES).present?
|
||||
raise Carto::ParamCombinationInvalidError.new(:types, VALID_TYPES)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
class Carto::Api::SearchPreviewPresenter
|
||||
|
||||
def initialize(dashboard_search_result:, limit:, current_viewer:, context:)
|
||||
@dashboard_search_result = dashboard_search_result
|
||||
@limit = limit
|
||||
@current_viewer = current_viewer
|
||||
@context = context
|
||||
end
|
||||
|
||||
def to_poro
|
||||
result = (poro_tags + poro_visualizations).first(@limit)
|
||||
{
|
||||
result: result,
|
||||
total_count: @dashboard_search_result.total_count
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def poro_tags
|
||||
@dashboard_search_result.tags.map do |tag|
|
||||
Carto::Api::TagPresenter.new(tag, @current_viewer, @context).to_search_preview_poro
|
||||
end
|
||||
end
|
||||
|
||||
def poro_visualizations
|
||||
@dashboard_search_result.visualizations.map do |visualization|
|
||||
Carto::Api::VisualizationPresenter.new(visualization, @current_viewer, @context).to_search_preview_poro
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
module Carto
|
||||
module Api
|
||||
class SnapshotPresenter
|
||||
def initialize(snapshot)
|
||||
@snapshot = snapshot
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
id: @snapshot.id,
|
||||
created_at: @snapshot.created_at,
|
||||
updated_at: @snapshot.updated_at,
|
||||
state: @snapshot.state,
|
||||
user: Carto::Api::UserPresenter.new(@snapshot.user).to_public_poro
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,84 @@
|
||||
module Carto
|
||||
module Api
|
||||
class SnapshotsController < ::Api::ApplicationController
|
||||
include ControllerHelper
|
||||
|
||||
ssl_required :index, :show, :create, :update, :destroy
|
||||
|
||||
before_filter :load_visualization,
|
||||
:check_visualization_viewable
|
||||
before_filter :load_snapshot,
|
||||
:owners_only, only: [:show, :update, :destroy]
|
||||
|
||||
rescue_from LoadError,
|
||||
UnauthorizedError,
|
||||
UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
|
||||
def index
|
||||
snapshots = @visualization.snapshots.where(user_id: current_viewer.id)
|
||||
|
||||
presentation = snapshots.map do |snapshot|
|
||||
SnapshotPresenter.new(snapshot).to_hash
|
||||
end
|
||||
|
||||
render json: presentation
|
||||
end
|
||||
|
||||
def show
|
||||
render json: SnapshotPresenter.new(@snapshot).to_hash
|
||||
end
|
||||
|
||||
def create
|
||||
snapshot = Snapshot.create!(user_id: current_viewer.id,
|
||||
visualization_id: @visualization.id,
|
||||
state: params[:state])
|
||||
|
||||
render json: SnapshotPresenter.new(snapshot).to_hash, status: :created
|
||||
rescue ActiveRecord::RecordInvalid => exception
|
||||
message = exception.record.errors.full_messages.join(', ')
|
||||
raise UnprocesableEntityError.new(message)
|
||||
end
|
||||
|
||||
def update
|
||||
@snapshot.update_attributes!(state: params[:state])
|
||||
|
||||
render json: SnapshotPresenter.new(@snapshot).to_hash
|
||||
rescue ActiveRecord::RecordInvalid => exception
|
||||
message = exception.record.errors.full_messages.join(', ')
|
||||
raise UnprocesableEntityError.new(message)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@snapshot.destroy
|
||||
|
||||
render json: Hash.new, status: :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_visualization
|
||||
@visualization = Visualization.find(params[:visualization_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise LoadError.new('Visualization not found')
|
||||
end
|
||||
|
||||
def check_visualization_viewable
|
||||
unless @visualization.is_viewable_by_user?(current_viewer)
|
||||
raise UnauthorizedError.new
|
||||
end
|
||||
end
|
||||
|
||||
def load_snapshot
|
||||
@snapshot = Snapshot.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise LoadError.new('Snapshot not found')
|
||||
end
|
||||
|
||||
def owners_only
|
||||
unless @snapshot.user_id == current_viewer.id
|
||||
raise UnauthorizedError.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
module Carto
|
||||
module Api
|
||||
class StatePresenter
|
||||
def initialize(state)
|
||||
@state = state
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
id: @state.id,
|
||||
visualization_id: @state.visualization_id,
|
||||
created_at: @state.created_at,
|
||||
updated_at: @state.updated_at,
|
||||
json: @state.json
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
module Carto
|
||||
module Api
|
||||
class StatesController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :update
|
||||
|
||||
before_filter :load_visualization,
|
||||
:check_writer, only: :update
|
||||
|
||||
rescue_from LoadError,
|
||||
UnauthorizedError,
|
||||
UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
|
||||
def update
|
||||
@visualization.state.json = params[:json]
|
||||
@visualization.state.save!
|
||||
|
||||
render json: StatePresenter.new(@visualization.state).to_hash
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
message = @visualization.errors.full_messages.join(', ')
|
||||
raise UnprocesableEntityError.new(message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_visualization
|
||||
@visualization = Visualization.find(params[:visualization_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise LoadError.new('Visualization not found')
|
||||
end
|
||||
|
||||
def check_writer
|
||||
unless @visualization.writable_by?(current_viewer)
|
||||
raise UnauthorizedError.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
require_dependency 'carto/uuidhelper'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class StaticNotificationsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :update
|
||||
before_filter :load_static_notifications, only: [:update]
|
||||
|
||||
rescue_from StandardError, with: :rescue_from_standard_error
|
||||
rescue_from Carto::LoadError, with: :rescue_from_carto_error
|
||||
|
||||
def update
|
||||
category = params[:category].to_sym
|
||||
@notifications.notifications[category] = params[:notifications]
|
||||
if @notifications.save
|
||||
render_jsonp({ notifications: @notifications.notifications[category] }, 200)
|
||||
else
|
||||
render_jsonp({ errors: @notifications.errors.to_h }, 422)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_static_notifications
|
||||
@notifications = Carto::User.find(current_user.id).static_notifications
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class SynchronizationPresenter
|
||||
|
||||
def initialize(synchronization)
|
||||
@synchronization = synchronization
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return nil if @synchronization.nil?
|
||||
|
||||
{
|
||||
checksum: @synchronization.checksum,
|
||||
created_at: @synchronization.created_at,
|
||||
error_code: @synchronization.error_code,
|
||||
error_message: @synchronization.error_message,
|
||||
id: @synchronization.id,
|
||||
interval: @synchronization.interval,
|
||||
modified_at: @synchronization.modified_at,
|
||||
name: @synchronization.name,
|
||||
ran_at: @synchronization.ran_at,
|
||||
retried_times: @synchronization.retried_times,
|
||||
run_at: @synchronization.run_at,
|
||||
service_item_id: @synchronization.service_item_id,
|
||||
service_name: @synchronization.service_name,
|
||||
state: @synchronization.state,
|
||||
updated_at: @synchronization.updated_at,
|
||||
url: @synchronization.url,
|
||||
user_id: @synchronization.user_id,
|
||||
content_guessing: @synchronization.content_guessing,
|
||||
etag: @synchronization.etag,
|
||||
log_id: @synchronization.log_id,
|
||||
quoted_fields_guessing: @synchronization.quoted_fields_guessing,
|
||||
type_guessing: @synchronization.type_guessing,
|
||||
from_external_source: @synchronization.from_external_source?,
|
||||
visualization_id: @synchronization.visualization_id
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
module Carto
|
||||
module Api
|
||||
class SynchronizationsController < ::Api::ApplicationController
|
||||
|
||||
ssl_required :show, :index, :syncing?
|
||||
|
||||
before_filter :load_synchronization, only: [:show, :syncing?]
|
||||
|
||||
def show
|
||||
render_jsonp(@synchronization)
|
||||
rescue => exception
|
||||
CartoDB.notify_exception(exception)
|
||||
head(404)
|
||||
end
|
||||
|
||||
def index
|
||||
synchronizations = Carto::Synchronization.where(user_id: current_user.id)
|
||||
representation = synchronizations.map(&:to_hash)
|
||||
response = {
|
||||
synchronizations: representation,
|
||||
total_entries: synchronizations.count
|
||||
}
|
||||
render_jsonp(response)
|
||||
rescue => exception
|
||||
CartoDB.notify_exception(exception)
|
||||
head(404)
|
||||
end
|
||||
|
||||
def syncing?
|
||||
render_jsonp( { state: @synchronization.state } )
|
||||
rescue => exception
|
||||
CartoDB.notify_exception(exception)
|
||||
head(404)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_synchronization
|
||||
@synchronization = Carto::Synchronization::where(id: params[:id]).first
|
||||
head(404) and return unless @synchronization
|
||||
head(401) and return unless @synchronization.authorize?(current_user)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
module Carto
|
||||
module Api
|
||||
class TablePresenter
|
||||
def initialize(table, current_viewer, context)
|
||||
@table = table
|
||||
@current_viewer = current_viewer
|
||||
@context = context
|
||||
end
|
||||
|
||||
def to_poro
|
||||
{
|
||||
id: @table.id,
|
||||
name: qualified_table_name,
|
||||
privacy: @table.privacy_text,
|
||||
schema: @table.schema,
|
||||
updated_at: @table.updated_at,
|
||||
rows_counted: @table.rows_estimated,
|
||||
table_size: @table.table_size,
|
||||
map_id: @table.map_id,
|
||||
description: @table.description,
|
||||
geometry_types: @table.geometry_types,
|
||||
table_visualization: VisualizationPresenter.new(@table.table_visualization, @current_viewer, @context).to_poro,
|
||||
dependent_visualizations: @table.fully_dependent_visualizations.map do |v|
|
||||
VisualizationPresenter.new(v, @current_viewer, @context).to_summarized_poro
|
||||
end,
|
||||
non_dependent_visualizations: @table.partially_dependent_visualizations.map do |v|
|
||||
VisualizationPresenter.new(v, @current_viewer, @context).to_summarized_poro
|
||||
end,
|
||||
synchronization: SynchronizationPresenter.new(@table.synchronization).to_poro
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def qualified_table_name
|
||||
owner = @table.owner
|
||||
if @current_viewer && owner && owner.id != @current_viewer.id
|
||||
"#{owner.sql_safe_database_schema}.#{@table.name}"
|
||||
else
|
||||
@table.name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,100 @@
|
||||
require_relative '../../../models/carto/permission'
|
||||
require_relative '../../../models/carto/user_table'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class TablesController < ::Api::ApplicationController
|
||||
ssl_required :show, :create, :update
|
||||
|
||||
before_filter :set_start_time
|
||||
|
||||
before_filter :load_user_table, only: [:show, :update]
|
||||
before_filter :read_privileges?, only: [:show]
|
||||
before_filter :write_privileges?, only: [:update]
|
||||
|
||||
def show
|
||||
table = @user_table.service
|
||||
render_jsonp(TablePresenter.new(table, current_user, self).to_poro.merge(schema: table.schema(reload: true)))
|
||||
end
|
||||
|
||||
# Very basic controller method to simply make blank tables
|
||||
# All other table creation things are controlled via the imports_controller#create
|
||||
def create
|
||||
table = ::Table.new
|
||||
table.user_id = current_user.id
|
||||
|
||||
tables = Carto::Db::UserSchema.new(current_user).table_names
|
||||
table.name = Carto::ValidTableNameProposer.new.propose_valid_table_name(params[:name], taken_names: tables)
|
||||
|
||||
table.description = params[:description] if params[:description]
|
||||
table.the_geom_type = params[:the_geom_type] if params[:the_geom_type]
|
||||
table.force_schema = params[:schema] if params[:schema]
|
||||
table.tags = params[:tags] if params[:tags]
|
||||
table.import_from_query = params[:from_query] if params[:from_query]
|
||||
|
||||
if table.valid? && table.save
|
||||
render_jsonp(TablePresenter.new(table, current_user, self).to_poro, 200, location: "/tables/#{table.id}")
|
||||
|
||||
table_visualization = table.table_visualization
|
||||
if table_visualization
|
||||
current_viewer_id = current_viewer.id
|
||||
Carto::Tracking::Events::CreatedDataset.new(current_viewer_id,
|
||||
visualization_id: table_visualization.id,
|
||||
user_id: current_viewer_id,
|
||||
origin: 'blank').report
|
||||
end
|
||||
else
|
||||
CartoDB::Logger.error(message: 'Error on tables#create', errors: table.errors.full_messages)
|
||||
render_jsonp({ description: table.errors.full_messages, stack: table.errors.full_messages }, 400)
|
||||
end
|
||||
rescue CartoDB::QuotaExceeded
|
||||
current_viewer_id = current_viewer.id
|
||||
Carto::Tracking::Events::ExceededQuota.new(current_viewer_id,
|
||||
user_id: current_viewer_id).report
|
||||
render_jsonp({ errors: ['You have reached your table quota'] }, 400)
|
||||
end
|
||||
|
||||
def update
|
||||
# TODO This endpoint is only used to geocode from editor, passing `latitude_column` and `longitude_column`
|
||||
# TODO It also supports attributes assignement, but this is not called from our frontend
|
||||
table = @user_table.service
|
||||
warnings = []
|
||||
|
||||
# TODO: this is bad, passing all params blindly to the table object
|
||||
@user_table.assign_attributes(params.symbolize_keys.reject { |k, _| k == :name })
|
||||
if params.keys.include?('latitude_column') && params.keys.include?('longitude_column')
|
||||
latitude_column = params[:latitude_column] == 'nil' ? nil : params[:latitude_column].try(:to_sym)
|
||||
longitude_column = params[:longitude_column] == 'nil' ? nil : params[:longitude_column].try(:to_sym)
|
||||
table.georeference_from!(latitude_column: latitude_column, longitude_column: longitude_column)
|
||||
table.update_bounding_box
|
||||
render_jsonp(TablePresenter.new(table, current_user, self).to_poro.merge(warnings: warnings))
|
||||
return
|
||||
end
|
||||
|
||||
if @user_table.save
|
||||
render_jsonp(TablePresenter.new(table, current_user, self).to_poro.merge(warnings: warnings))
|
||||
else
|
||||
render_jsonp({ errors: table.errors.full_messages }, 400)
|
||||
end
|
||||
rescue => e
|
||||
CartoDB::Logger.error(message: 'Error updating table', exception: e)
|
||||
render_jsonp({ errors: [translate_error(e.message.split("\n").first)] }, 400)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user_table
|
||||
@user_table = Carto::Helpers::TableLocator.new.get_by_id_or_name(params[:id], current_user)
|
||||
raise RecordNotFound unless @user_table
|
||||
end
|
||||
|
||||
def read_privileges?
|
||||
head(403) unless current_user && @user_table.visualization.is_viewable_by_user?(current_user)
|
||||
end
|
||||
|
||||
def write_privileges?
|
||||
head(401) unless current_user && @user_table.visualization.writable_by?(current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
class Carto::Api::TagPresenter
|
||||
|
||||
def initialize(tag_name, current_viewer, context)
|
||||
@tag_name = tag_name
|
||||
@current_viewer = current_viewer
|
||||
@context = context
|
||||
end
|
||||
|
||||
def to_search_preview_poro
|
||||
{
|
||||
type: "tag",
|
||||
name: @tag_name,
|
||||
url: CartoDB.url(@context, "tag_search", user: @current_viewer, params: { q: @tag_name })
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
require_dependency 'carto/controller_helper'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class TagsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
include PagedSearcher
|
||||
|
||||
ssl_required
|
||||
|
||||
before_filter :load_parameters
|
||||
|
||||
rescue_from StandardError, with: :rescue_from_standard_error
|
||||
rescue_from Carto::ParamCombinationInvalidError, with: :rescue_from_carto_error
|
||||
|
||||
DEFAULT_TAGS_PER_PAGE = 6
|
||||
|
||||
def index
|
||||
query_builder = tag_query_builder
|
||||
result = query_builder.build_paged(@page, @per_page)
|
||||
total_count = query_builder.total_count
|
||||
|
||||
render json: format_response(result, total_count)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tag_query_builder
|
||||
query_builder = Carto::TagQueryBuilder.new
|
||||
.with_types(@types)
|
||||
.with_partial_match(@pattern)
|
||||
|
||||
return query_builder.with_owned_by_or_shared_with_user_id(current_viewer.id) if @include_shared
|
||||
query_builder.with_owned_by_user_id(current_viewer.id)
|
||||
end
|
||||
|
||||
def load_parameters
|
||||
@page, @per_page = page_per_page_params(default_per_page: DEFAULT_TAGS_PER_PAGE)
|
||||
|
||||
@pattern = params[:q]
|
||||
@include_shared = params[:include_shared] == 'true'
|
||||
|
||||
@types = params.fetch(:types, "").split(',')
|
||||
if (@types - Carto::Visualization::VALID_TYPES).present?
|
||||
raise Carto::ParamCombinationInvalidError.new(:types, Carto::Visualization::VALID_TYPES)
|
||||
end
|
||||
end
|
||||
|
||||
def format_response(result, total_count)
|
||||
paged_result(
|
||||
result: result,
|
||||
total_count: total_count,
|
||||
page: @page,
|
||||
per_page: @per_page,
|
||||
params: params
|
||||
) { |params| api_v3_users_tags_url(params) }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
module Carto
|
||||
module Api
|
||||
class TemplatePresenter
|
||||
|
||||
def initialize(template)
|
||||
@template = template
|
||||
end
|
||||
|
||||
# When used inside a list, parent will be a " items: [ ... ] "
|
||||
def public_values
|
||||
{
|
||||
id: @template.id,
|
||||
title: @template.title,
|
||||
description: @template.description,
|
||||
source_visualization: {
|
||||
id: @template.source_visualization_id
|
||||
},
|
||||
min_supported_version: @template.min_supported_version,
|
||||
max_supported_version: @template.max_supported_version,
|
||||
code: @template.code,
|
||||
organization: {
|
||||
id: @template.organization_id
|
||||
},
|
||||
required_tables: @template.required_tables,
|
||||
created_at: @template.created_at
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,146 @@
|
||||
module Carto
|
||||
module Api
|
||||
class TemplatesController < ::Api::ApplicationController
|
||||
|
||||
ssl_required :index, :show, :create, :update, :destroy, :related_templates_by_visualization,
|
||||
:related_templates_by_table
|
||||
before_filter :load_template, only: [ :show, :update, :destroy ]
|
||||
before_filter :check_feature_flag
|
||||
|
||||
def index
|
||||
templates = Carto::Template.where(organization_id: current_user.organization_id)
|
||||
.order(:created_at)
|
||||
.reverse_order
|
||||
.all
|
||||
|
||||
render_jsonp({ items: templates.map { |template| Carto::Api::TemplatePresenter.new(template).public_values } })
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e)
|
||||
render json: { error: [e.message] }, status: 400
|
||||
end
|
||||
|
||||
def show
|
||||
render_jsonp(Carto::Api::TemplatePresenter.new(@template).public_values)
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e)
|
||||
render json: { error: [e.message] }, status: 400
|
||||
end
|
||||
|
||||
def create
|
||||
@stats_aggregator.timing('templates.create') do
|
||||
|
||||
begin
|
||||
@template = Carto::Template.new({
|
||||
source_visualization_id: params['source_visualization_id'],
|
||||
title: params['title'],
|
||||
description: params.fetch('description', ''),
|
||||
min_supported_version: params['min_supported_version'],
|
||||
max_supported_version: params['max_supported_version'],
|
||||
code: params.fetch('code', ''),
|
||||
organization_id: current_user.organization_id,
|
||||
required_tables: params.fetch('required_tables', [])
|
||||
})
|
||||
|
||||
result = @stats_aggregator.timing('save') do
|
||||
@template.save
|
||||
end
|
||||
|
||||
render_jsonp({ :errors => ["#{@template.errors.messages.values.join(',')}"] }, 400) and return unless result
|
||||
|
||||
render_jsonp(Carto::Api::TemplatePresenter.new(@template).public_values)
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e)
|
||||
render json: { error: [e.message] }, status: 400
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@stats_aggregator.timing('templates.update') do
|
||||
|
||||
begin
|
||||
@template.title = params['title']
|
||||
@template.description = params.fetch('description', '')
|
||||
@template.min_supported_version = params['min_supported_version']
|
||||
@template.max_supported_version = params['max_supported_version']
|
||||
@template.code = params.fetch('code', '')
|
||||
@template.required_tables = params.fetch('required_tables', [])
|
||||
|
||||
result = @stats_aggregator.timing('save') do
|
||||
@template.save
|
||||
end
|
||||
|
||||
render_jsonp({ :errors => ["#{@template.errors.messages.values.join(',')}"] }, 400) and return unless result
|
||||
|
||||
@template.reload
|
||||
|
||||
render_jsonp(Carto::Api::TemplatePresenter.new(@template).public_values)
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e)
|
||||
render json: { error: [e.message] }, status: 400
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@stats_aggregator.timing('templates.destroy') do
|
||||
|
||||
begin
|
||||
@stats_aggregator.timing('delete') do
|
||||
@template.delete
|
||||
end
|
||||
|
||||
head :ok
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e)
|
||||
render json: { error: [e.message] }, status: 400
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def related_templates_by_visualization
|
||||
return head(400) if params[:id].nil?
|
||||
vis = Carto::Visualization.where(id: params[:id]).first
|
||||
return head(400) if vis.nil?
|
||||
|
||||
templates = vis.related_templates
|
||||
|
||||
render_jsonp({ items: templates.map { |template| Carto::Api::TemplatePresenter.new(template).public_values } })
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e)
|
||||
render json: { error: [e.message] }, status: 400
|
||||
end
|
||||
|
||||
def related_templates_by_table
|
||||
user_table = Carto::Helpers::TableLocator.new.get_by_id_or_name(params.fetch('id'), current_user)
|
||||
templates = user_table.service.related_templates
|
||||
|
||||
render_jsonp(items: templates.map { |template| Carto::Api::TemplatePresenter.new(template).public_values })
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e)
|
||||
render json: { error: [e.message] }, status: 400
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_template
|
||||
@template = Carto::Template.where(id: params[:id]).first
|
||||
return render json: { errors: ["Template #{params[:id]} not found"] }, status: 404 if @template.nil?
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e)
|
||||
render json: { error: [e.message] }, status: 400
|
||||
end
|
||||
|
||||
def check_feature_flag
|
||||
unless current_user.has_feature_flag?('templated_workflows')
|
||||
render json: { error: "Endpoint disabled for this user" }, status: 403
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
module Carto
|
||||
module Api
|
||||
class UserCreationPresenter
|
||||
|
||||
def initialize(user_creation)
|
||||
@user_creation = user_creation
|
||||
end
|
||||
|
||||
def to_poro
|
||||
{
|
||||
id: @user_creation.id,
|
||||
username: @user_creation.username,
|
||||
email: @user_creation.email,
|
||||
organization_id: @user_creation.organization_id,
|
||||
google_sign_in: @user_creation.google_sign_in,
|
||||
requires_validation_email: @user_creation.requires_validation_email?,
|
||||
state: @user_creation.state,
|
||||
created_at: @user_creation.created_at,
|
||||
updated_at: @user_creation.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
require_relative 'user_creation_presenter'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class UserCreationsController < ::Api::ApplicationController
|
||||
|
||||
skip_before_filter :api_authorization_required
|
||||
skip_before_filter :http_header_authentication, only: [:show]
|
||||
|
||||
ssl_required :show
|
||||
|
||||
before_filter :load_user_creation, only: :show
|
||||
|
||||
def show
|
||||
if @user_creation.autologin?
|
||||
params[:username] = @user_creation.username
|
||||
authenticate!(:user_creation, scope: @user_creation.username)
|
||||
end
|
||||
render_jsonp(UserCreationPresenter.new(@user_creation).to_poro)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user_creation
|
||||
@user_creation = Carto::UserCreation.where(id: params[:id]).first
|
||||
render_404 and return false unless @user_creation
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,307 @@
|
||||
require_relative 'group_presenter'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class UserPresenter
|
||||
include AccountTypeHelper
|
||||
|
||||
def initialize(user,
|
||||
fetch_groups: false,
|
||||
current_viewer: nil,
|
||||
fetch_db_size: true,
|
||||
fetch_basemaps: false,
|
||||
fetch_profile: true)
|
||||
@user = user
|
||||
@fetch_groups = fetch_groups
|
||||
@current_viewer = current_viewer
|
||||
@fetch_db_size = fetch_db_size
|
||||
@fetch_basemaps = fetch_basemaps
|
||||
@fetch_profile = fetch_profile
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return {} if @user.nil?
|
||||
return to_public_poro unless current_viewer && @user.viewable_by?(current_viewer)
|
||||
|
||||
poro = {
|
||||
id: @user.id,
|
||||
name: @user.name,
|
||||
last_name: @user.last_name,
|
||||
username: @user.username,
|
||||
email: @user.email,
|
||||
avatar_url: @user.avatar_url,
|
||||
website: @user.website,
|
||||
description: @user.description,
|
||||
location: @user.location,
|
||||
twitter_username: @user.twitter_username,
|
||||
disqus_shortname: @user.disqus_shortname,
|
||||
available_for_hire: @user.available_for_hire,
|
||||
base_url: @user.public_url,
|
||||
google_maps_query_string: @user.google_maps_query_string,
|
||||
quota_in_bytes: @user.quota_in_bytes,
|
||||
table_count: @user.table_count,
|
||||
viewer: @user.viewer?,
|
||||
org_admin: @user.organization_admin?,
|
||||
public_visualization_count: @user.public_visualization_count,
|
||||
all_visualization_count: @user.all_visualization_count,
|
||||
org_user: @user.organization_id.present?,
|
||||
remove_logo: @user.remove_logo?
|
||||
}
|
||||
|
||||
if fetch_groups
|
||||
poro[:groups] = @user.groups ? @user.groups.map { |g| Carto::Api::GroupPresenter.new(g).to_poro } : []
|
||||
end
|
||||
|
||||
poro[:basemaps] = @user.basemaps if fetch_basemaps
|
||||
|
||||
poro[:db_size_in_bytes] = @user.db_size_in_bytes if fetch_db_size
|
||||
|
||||
if fetch_profile
|
||||
poro[:industry] = @user.industry
|
||||
poro[:company_employees] = @user.company_employees
|
||||
poro[:use_case] = @user.use_case
|
||||
poro[:company] = @user.company
|
||||
poro[:phone] = @user.phone
|
||||
poro[:job_role] = @user.job_role
|
||||
end
|
||||
|
||||
poro
|
||||
end
|
||||
|
||||
def to_eumapi_poro
|
||||
presentation = to_poro
|
||||
|
||||
presentation.delete(:id)
|
||||
presentation[:soft_geocoding_limit] = @user.soft_geocoding_limit
|
||||
presentation[:api_key] = @user.api_key
|
||||
|
||||
presentation
|
||||
end
|
||||
|
||||
def to_public_poro
|
||||
return {} if @user.nil?
|
||||
|
||||
poro = {
|
||||
id: @user.id,
|
||||
username: @user.username,
|
||||
name: @user.name,
|
||||
last_name: @user.last_name,
|
||||
avatar_url: @user.avatar_url,
|
||||
base_url: @user.public_url,
|
||||
google_maps_query_string: @user.google_maps_query_string,
|
||||
disqus_shortname: @user.disqus_shortname,
|
||||
viewer: @user.viewer?,
|
||||
org_admin: @user.organization_admin?,
|
||||
org_user: @user.organization_id.present?,
|
||||
remove_logo: @user.remove_logo?
|
||||
}
|
||||
|
||||
if fetch_groups
|
||||
poro[:groups] = @user.groups ? @user.groups.map { |g| Carto::Api::GroupPresenter.new(g).to_poro } : []
|
||||
end
|
||||
|
||||
poro
|
||||
end
|
||||
|
||||
def data(options = {})
|
||||
return {} if @user.nil?
|
||||
|
||||
calls = @user.get_api_calls(from: @user.last_billing_cycle, to: Date.today)
|
||||
calls.fill(0, calls.size..29)
|
||||
|
||||
db_size_in_bytes = @user.db_size_in_bytes
|
||||
|
||||
data = {
|
||||
id: @user.id,
|
||||
email: @user.email,
|
||||
name: @user.name,
|
||||
last_name: @user.last_name,
|
||||
created_at: @user.created_at,
|
||||
username: @user.username,
|
||||
state: @user.state,
|
||||
account_type: @user.account_type,
|
||||
account_type_display_name: plan_name(@user.account_type),
|
||||
table_quota: @user.table_quota,
|
||||
public_map_quota: @user.public_map_quota,
|
||||
regular_api_key_quota: @user.regular_api_key_quota,
|
||||
table_count: @user.table_count,
|
||||
viewer: @user.viewer?,
|
||||
industry: @user.industry,
|
||||
company_employees: @user.company_employees,
|
||||
use_case: @user.use_case,
|
||||
company: @user.company,
|
||||
phone: @user.phone,
|
||||
job_role: @user.job_role,
|
||||
org_admin: @user.organization_admin?,
|
||||
public_visualization_count: @user.public_visualization_count,
|
||||
public_privacy_map_count: @user.public_privacy_visualization_count,
|
||||
link_privacy_map_count: @user.link_privacy_visualization_count,
|
||||
password_privacy_map_count: @user.password_privacy_visualization_count,
|
||||
owned_visualization_count: @user.owned_visualization_count,
|
||||
all_visualization_count: @user.all_visualization_count,
|
||||
visualization_count: @user.visualization_count,
|
||||
failed_import_count: failed_import_count,
|
||||
success_import_count: success_import_count,
|
||||
import_count: import_count,
|
||||
last_visualization_created_at: last_visualization_created_at,
|
||||
quota_in_bytes: @user.quota_in_bytes,
|
||||
db_size_in_bytes: db_size_in_bytes,
|
||||
db_size_in_megabytes: db_size_in_bytes.present? ? (db_size_in_bytes / (1024.0 * 1024.0)).round(2) : nil,
|
||||
remaining_table_quota: @user.remaining_table_quota,
|
||||
remaining_byte_quota: @user.remaining_quota(false, db_size_in_bytes).to_f,
|
||||
api_calls: calls,
|
||||
api_calls_quota: @user.organization_user? ? @user.organization.map_view_quota : @user.map_view_quota,
|
||||
api_calls_block_price: @user.organization_user? ? @user.organization.map_view_block_price : @user.map_view_block_price,
|
||||
geocoding: {
|
||||
quota: @user.organization_user? ? @user.organization.geocoding_quota : @user.geocoding_quota,
|
||||
block_price: @user.organization_user? ? @user.organization.geocoding_block_price : @user.geocoding_block_price,
|
||||
monthly_use: @user.organization_user? ? @user.organization.get_geocoding_calls : @user.get_geocoding_calls,
|
||||
hard_limit: @user.hard_geocoding_limit?
|
||||
},
|
||||
here_isolines: {
|
||||
quota: @user.organization_user? ? @user.organization.here_isolines_quota : @user.here_isolines_quota,
|
||||
block_price: @user.organization_user? ? @user.organization.here_isolines_block_price : @user.here_isolines_block_price,
|
||||
monthly_use: @user.organization_user? ? @user.organization.get_here_isolines_calls : @user.get_here_isolines_calls,
|
||||
hard_limit: @user.hard_here_isolines_limit?
|
||||
},
|
||||
mapzen_routing: {
|
||||
quota: @user.organization_user? ? @user.organization.mapzen_routing_quota : @user.mapzen_routing_quota,
|
||||
block_price: @user.organization_user? ? @user.organization.mapzen_routing_block_price : @user.mapzen_routing_block_price,
|
||||
monthly_use: @user.organization_user? ? @user.organization.get_mapzen_routing_calls : @user.get_mapzen_routing_calls,
|
||||
hard_limit: @user.hard_mapzen_routing_limit?
|
||||
},
|
||||
geocoder_provider: @user.geocoder_provider,
|
||||
isolines_provider: @user.isolines_provider,
|
||||
routing_provider: @user.routing_provider,
|
||||
obs_snapshot: {
|
||||
quota: @user.organization_user? ? @user.organization.obs_snapshot_quota : @user.obs_snapshot_quota,
|
||||
block_price: @user.organization_user? ? @user.organization.obs_snapshot_block_price : @user.obs_snapshot_block_price,
|
||||
monthly_use: @user.organization_user? ? @user.organization.get_obs_snapshot_calls : @user.get_obs_snapshot_calls,
|
||||
hard_limit: @user.hard_obs_snapshot_limit?
|
||||
},
|
||||
obs_general: {
|
||||
quota: @user.organization_user? ? @user.organization.obs_general_quota : @user.obs_general_quota,
|
||||
block_price: @user.organization_user? ? @user.organization.obs_general_block_price : @user.obs_general_block_price,
|
||||
monthly_use: @user.organization_user? ? @user.organization.get_obs_general_calls : @user.get_obs_general_calls,
|
||||
hard_limit: @user.hard_obs_general_limit?
|
||||
},
|
||||
twitter: {
|
||||
enabled: @user.twitter_datasource_enabled,
|
||||
quota: @user.organization_user? ? @user.organization.twitter_datasource_quota : @user.twitter_datasource_quota,
|
||||
block_price: @user.organization_user? ? @user.organization.twitter_datasource_block_price : @user.twitter_datasource_block_price,
|
||||
block_size: @user.organization_user? ? @user.organization.twitter_datasource_block_size : @user.twitter_datasource_block_size,
|
||||
monthly_use: @user.organization_user? ? @user.organization.twitter_imports_count : @user.twitter_imports_count,
|
||||
hard_limit: @user.hard_twitter_datasource_limit,
|
||||
customized_config: CartoDB::Datasources::DatasourcesFactory.customized_config?(CartoDB::Datasources::Search::Twitter::DATASOURCE_NAME, @user)
|
||||
},
|
||||
salesforce: {
|
||||
enabled: @user.organization_user? ? @user.organization.salesforce_datasource_enabled : @user.salesforce_datasource_enabled
|
||||
},
|
||||
mailchimp: {
|
||||
enabled: Carto::AccountType.new.mailchimp?(@user)
|
||||
},
|
||||
billing_period: @user.last_billing_cycle,
|
||||
next_billing_period: @user.next_billing_cycle,
|
||||
api_key: @user.api_key,
|
||||
layers: @user.layers.map { |layer|
|
||||
Carto::Api::LayerPresenter.new(layer).to_poro
|
||||
},
|
||||
trial_ends_at: @user.trial_ends_at,
|
||||
upgraded_at: @user.upgraded_at,
|
||||
show_trial_reminder: @user.trial_ends_at.present?,
|
||||
show_upgraded_message: (@user.account_type.downcase != 'free' && @user.upgraded_at && @user.upgraded_at + 15.days > Date.today ? true : false),
|
||||
actions: {
|
||||
private_tables: @user.private_tables_enabled,
|
||||
private_maps: @user.private_maps_enabled?,
|
||||
remove_logo: @user.remove_logo?,
|
||||
sync_tables: @user.sync_tables_enabled,
|
||||
google_maps_geocoder_enabled: @user.google_maps_geocoder_enabled?,
|
||||
google_maps_enabled: @user.google_maps_enabled?,
|
||||
engine_enabled: @user.engine_enabled?,
|
||||
builder_enabled: @user.builder_enabled?,
|
||||
mobile_sdk_enabled: @user.mobile_sdk_enabled?
|
||||
},
|
||||
limits: {
|
||||
concurrent_syncs: CartoDB::PlatformLimits::Importer::UserConcurrentSyncsAmount::MAX_SYNCS_PER_USER,
|
||||
concurrent_imports: @user.max_concurrent_import_count,
|
||||
import_file_size: @user.max_import_file_size,
|
||||
import_table_rows: @user.max_import_table_row_count,
|
||||
max_layers: @user.max_layers
|
||||
},
|
||||
notification: @user.notification,
|
||||
avatar_url: @user.avatar,
|
||||
feature_flags: @user.feature_flag_names,
|
||||
base_url: @user.public_url,
|
||||
needs_password_confirmation: @user.needs_password_confirmation?,
|
||||
description: @user.description,
|
||||
website: @user.website,
|
||||
twitter_username: @user.twitter_username,
|
||||
disqus_shortname: @user.disqus_shortname,
|
||||
available_for_hire: @user.available_for_hire,
|
||||
location: @user.location,
|
||||
mfa_configured: @user.multifactor_authentication_configured?
|
||||
}
|
||||
|
||||
if @user.google_maps_geocoder_enabled? && (!@user.organization.present? || @user.organization_owner?)
|
||||
data[:google_maps_private_key] = @user.google_maps_private_key
|
||||
end
|
||||
|
||||
if @user.organization.present?
|
||||
data[:organization] = Carto::Api::OrganizationPresenter.new(@user.organization).to_poro
|
||||
data[:organization][:available_quota_for_user] = @user.organization.unassigned_quota + @user.quota_in_bytes
|
||||
end
|
||||
|
||||
if !@user.groups.nil?
|
||||
data[:groups] = @user.groups.map { |g| Carto::Api::GroupPresenter.new(g).to_poro }
|
||||
end
|
||||
|
||||
if @user.mobile_sdk_enabled?
|
||||
data[:mobile_apps] = {
|
||||
mobile_xamarin: @user.mobile_xamarin,
|
||||
mobile_custom_watermark: @user.mobile_custom_watermark,
|
||||
mobile_offline_maps: @user.mobile_offline_maps,
|
||||
mobile_gis_extension: @user.mobile_gis_extension,
|
||||
mobile_max_open_users: @user.mobile_max_open_users,
|
||||
mobile_max_private_users: @user.mobile_max_private_users
|
||||
}
|
||||
end
|
||||
|
||||
if options[:extended]
|
||||
# TODO: This fields are pending migration
|
||||
data.merge({
|
||||
:real_table_count => @user.real_tables.size,
|
||||
:last_active_time => @user.get_last_active_time
|
||||
})
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_viewer, :current_user, :fetch_groups, :fetch_db_size, :fetch_basemaps, :fetch_profile
|
||||
|
||||
def failed_import_count
|
||||
Carto::DataImport.where(user_id: @user.id, state: 'failure').count
|
||||
end
|
||||
|
||||
def success_import_count
|
||||
Carto::DataImport.where(user_id: @user.id, state: 'complete').count
|
||||
end
|
||||
|
||||
def import_count
|
||||
DataImport.where(user_id: @user.id).count
|
||||
end
|
||||
|
||||
def last_visualization_created_at
|
||||
row_data = Carto::VisualizationQueryBuilder.new
|
||||
.with_user_id(@user.id)
|
||||
.with_order(:created_at, :desc)
|
||||
.build_paged(1, 1)
|
||||
.pluck(:created_at)
|
||||
row_data.nil? ? nil : row_data[0]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class UserTablePresenter
|
||||
# options:
|
||||
# - accessible_dependent_derived_maps
|
||||
|
||||
PRIVACY_PRIVATE = 0
|
||||
PRIVACY_PUBLIC = 1
|
||||
PRIVACY_LINK = 2
|
||||
|
||||
PRIVACY_VALUES_TO_TEXTS = {
|
||||
PRIVACY_PRIVATE => 'private',
|
||||
PRIVACY_PUBLIC => 'public',
|
||||
PRIVACY_LINK => 'link'
|
||||
}
|
||||
|
||||
def initialize(user_table, current_viewer, show_size_and_row_count: true)
|
||||
@user_table = user_table
|
||||
@current_viewer = current_viewer
|
||||
@presenter_cache = Carto::Api::PresenterCache.new
|
||||
@show_size_and_row_count = show_size_and_row_count
|
||||
end
|
||||
|
||||
def with_presenter_cache(presenter_cache)
|
||||
@presenter_cache = presenter_cache
|
||||
self
|
||||
end
|
||||
|
||||
def to_poro(accessible_dependent_derived_maps: false, context: nil)
|
||||
return {} if @user_table.nil?
|
||||
permission = @user_table.permission
|
||||
permission_presentation = Carto::Api::PermissionPresenter.new(
|
||||
permission, current_viewer: @current_viewer
|
||||
).with_presenter_cache(@presenter_cache).to_poro if permission
|
||||
|
||||
poro = {
|
||||
id: @user_table.id,
|
||||
name: @user_table.name_for_user(@current_viewer),
|
||||
permission: permission_presentation,
|
||||
geometry_types: @user_table.geometry_types,
|
||||
privacy: privacy_text(@user_table.privacy).upcase,
|
||||
updated_at: @user_table.updated_at
|
||||
}
|
||||
|
||||
if @user_table.is_owner?(@current_viewer)
|
||||
poro[:synchronization] = Carto::Api::SynchronizationPresenter.new(@user_table.synchronization).to_poro
|
||||
end
|
||||
|
||||
if show_size_and_row_count
|
||||
row_count_and_size = @user_table.row_count_and_size
|
||||
poro[:size] = row_count_and_size[:size]
|
||||
poro[:row_count] = row_count_and_size[:row_count]
|
||||
end
|
||||
|
||||
if accessible_dependent_derived_maps && context
|
||||
poro[:accessible_dependent_derived_maps] = derived_maps_to_presenter(context)
|
||||
end
|
||||
|
||||
poro
|
||||
end
|
||||
|
||||
def privacy_text(privacy)
|
||||
#TODO: This came from UserTable
|
||||
privacy == PRIVACY_LINK ? 'PUBLIC' : PRIVACY_VALUES_TO_TEXTS[privacy]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :show_size_and_row_count
|
||||
|
||||
def derived_maps_to_presenter(context)
|
||||
visualizations = @user_table.accessible_dependent_derived_maps
|
||||
visualizations.map { |v| Carto::Api::VisualizationPresenter.new(v, @current_viewer, context).to_poro }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,248 @@
|
||||
require_relative '../../helpers/avatar_helper'
|
||||
require_dependency 'carto/controller_helper'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class UsersController < ::Api::ApplicationController
|
||||
include OrganizationUsersHelper
|
||||
include AppAssetsHelper
|
||||
include MapsApiHelper
|
||||
include SqlApiHelper
|
||||
include CartoDB::ConfigUtils
|
||||
include FrontendConfigHelper
|
||||
include AccountTypeHelper
|
||||
include AvatarHelper
|
||||
include Carto::ControllerHelper
|
||||
|
||||
UPDATE_ME_FIELDS = %i(
|
||||
name last_name website description location twitter_username disqus_shortname available_for_hire company
|
||||
industry phone job_role company_employees use_case
|
||||
).freeze
|
||||
|
||||
PASSWORD_DOES_NOT_MATCH_MESSAGE = 'Password does not match'.freeze
|
||||
|
||||
ssl_required
|
||||
|
||||
before_action :optional_api_authorization, only: [:me]
|
||||
before_action :any_api_authorization_required, only: [:me_public]
|
||||
before_action :recalculate_user_db_size, only: [:me]
|
||||
skip_before_action :api_authorization_required, only: [:me, :me_public, :get_authenticated_users]
|
||||
skip_before_action :check_user_state, only: [:me, :delete_me]
|
||||
|
||||
rescue_from StandardError, with: :rescue_from_standard_error
|
||||
|
||||
def show
|
||||
render json: Carto::Api::UserPresenter.new(uri_user).data
|
||||
end
|
||||
|
||||
def me
|
||||
carto_viewer = current_viewer.present? ? Carto::User.find(current_viewer.id) : nil
|
||||
|
||||
cant_be_deleted_reason = carto_viewer.try(:cant_be_deleted_reason)
|
||||
can_be_deleted = carto_viewer.present? ? cant_be_deleted_reason.nil? : nil
|
||||
viewer_organization_notifications = carto_viewer ? organization_notifications(carto_viewer) : []
|
||||
viewer_unfiltered_notifications = carto_viewer ? unfiltered_organization_notifications(carto_viewer) : []
|
||||
|
||||
render json: {
|
||||
user_data: carto_viewer.present? ? Carto::Api::UserPresenter.new(carto_viewer).data : nil,
|
||||
default_fallback_basemap: carto_viewer.try(:default_basemap),
|
||||
config: frontend_config_hash(current_viewer),
|
||||
dashboard_notifications: carto_viewer.try(:notifications_for_category, :dashboard),
|
||||
organization_notifications: viewer_organization_notifications,
|
||||
unfiltered_organization_notifications: viewer_unfiltered_notifications,
|
||||
is_just_logged_in: carto_viewer.present? ? !!flash['logged'] : nil,
|
||||
is_first_time_viewing_dashboard: !carto_viewer.try(:dashboard_viewed_at),
|
||||
can_change_email: carto_viewer.try(:can_change_email?),
|
||||
auth_username_password_enabled: carto_viewer.try(:organization).try(:auth_username_password_enabled),
|
||||
can_change_password: carto_viewer.try(:can_change_password?),
|
||||
plan_name: carto_viewer.present? ? plan_name(carto_viewer.account_type) : nil,
|
||||
plan_url: carto_viewer.try(:plan_url, request.protocol),
|
||||
can_be_deleted: can_be_deleted,
|
||||
cant_be_deleted_reason: cant_be_deleted_reason,
|
||||
services: carto_viewer.try(:get_oauth_services),
|
||||
user_frontend_version: carto_viewer.try(:relevant_frontend_version) || CartoDB::Application.frontend_version,
|
||||
asset_host: carto_viewer.try(:asset_host),
|
||||
google_sign_in: carto_viewer.try(:google_sign_in),
|
||||
mfa_required: multifactor_authentication_required?
|
||||
}
|
||||
end
|
||||
|
||||
def update_me
|
||||
user = current_viewer
|
||||
attributes = params[:user]
|
||||
|
||||
if attributes.present?
|
||||
unless user.valid_password_confirmation(attributes[:password_confirmation])
|
||||
raise Carto::PasswordConfirmationError.new
|
||||
end
|
||||
update_user_attributes(user, attributes)
|
||||
raise Sequel::ValidationFailed.new('Validation failed') unless user.errors.try(:empty?) && user.valid?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
update_user_multifactor_authentication(user, attributes[:mfa])
|
||||
user.update_in_central
|
||||
user.save(raise_on_failure: true)
|
||||
end
|
||||
end
|
||||
|
||||
render_jsonp(Carto::Api::UserPresenter.new(user, current_viewer: current_viewer).to_poro)
|
||||
rescue CartoDB::CentralCommunicationFailure => e
|
||||
CartoDB::Logger.error(exception: e, user: user, params: params)
|
||||
render_jsonp({ errors: "There was a problem while updating your data. Please, try again." }, 422)
|
||||
rescue Sequel::ValidationFailed, ActiveRecord::RecordInvalid
|
||||
render_jsonp({ message: "Error updating your account details", errors: user.errors }, 400)
|
||||
rescue Carto::PasswordConfirmationError
|
||||
render_jsonp({ message: "Error updating your account details", errors: user.errors }, 403)
|
||||
end
|
||||
|
||||
def delete_me
|
||||
user = current_viewer
|
||||
|
||||
deletion_password_confirmation = params[:deletion_password_confirmation]
|
||||
|
||||
if user.needs_password_confirmation? && !user.validate_old_password(deletion_password_confirmation)
|
||||
render_jsonp({ message: "Error deleting user: #{PASSWORD_DOES_NOT_MATCH_MESSAGE}" }, 400) and return
|
||||
end
|
||||
|
||||
user.destroy_account
|
||||
|
||||
render_jsonp({ logout_url: logout_url }, 200)
|
||||
rescue CartoDB::CentralCommunicationFailure => e
|
||||
CartoDB::Logger.error(exception: e, message: 'Central error deleting user at CartoDB', user: @user)
|
||||
render_jsonp({ errors: "Error deleting user: #{e.user_message}" }, 422)
|
||||
rescue => e
|
||||
CartoDB.notify_exception(e, user: user.inspect)
|
||||
render_jsonp({ message: "Error deleting user: #{e.message}", errors: user.errors }, 400)
|
||||
end
|
||||
|
||||
def get_authenticated_users
|
||||
referer = request.env["HTTP_ORIGIN"].blank? ? request.env["HTTP_REFERER"] : %[#{request.env['HTTP_X_FORWARDED_PROTO']}://#{request.env["HTTP_HOST"]}]
|
||||
referer_match = /https?:\/\/([\w\-\.]+)(:[\d]+)?(\/((u|user)\/([\w\-\.]+)))?/.match(referer)
|
||||
if referer_match.nil?
|
||||
render json: { error: "Referer #{referer} does not match" }, status: 400 and return
|
||||
end
|
||||
|
||||
if session_user.nil?
|
||||
render json: {
|
||||
urls: [],
|
||||
username: nil,
|
||||
avatar_url: nil
|
||||
} and return
|
||||
end
|
||||
|
||||
subdomain = referer_match[1].gsub(CartoDB.session_domain, '').downcase
|
||||
# referer_match[6] is the username
|
||||
referer_organization_username = referer_match[6]
|
||||
render_auth_users_data(session_user, referer, subdomain, referer_organization_username)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unfiltered_organization_notifications(carto_viewer)
|
||||
carto_viewer.received_notifications.order('received_at DESC').limit(10).map do |n|
|
||||
Carto::Api::ReceivedNotificationPresenter.new(n).to_hash
|
||||
end
|
||||
end
|
||||
|
||||
def organization_notifications(carto_viewer)
|
||||
carto_viewer.received_notifications.unread.map { |n| Carto::Api::ReceivedNotificationPresenter.new(n).to_hash }
|
||||
end
|
||||
|
||||
def render_auth_users_data(user, referrer, subdomain, referrer_organization_username=nil)
|
||||
organization_name = nil
|
||||
|
||||
# It doesn't have a organization username component. We assume it's not a organization referer
|
||||
if referrer_organization_username.nil?
|
||||
# The user is authenticated but seeing another user dashboard
|
||||
if user.username != subdomain
|
||||
organization_name = CartoDB::UserOrganization.user_belongs_to_organization?(user.username)
|
||||
end
|
||||
else
|
||||
referrer_organization_username = referrer_organization_username.downcase
|
||||
|
||||
# The user is seeing its own organization dashboard
|
||||
if user.username == referrer_organization_username
|
||||
organization_name = subdomain
|
||||
# The user is seeing a organization dashboard, but not its one
|
||||
else
|
||||
# Authenticated with a user of the organization
|
||||
if user.organization && user.organization.name == subdomain
|
||||
organization_name = subdomain
|
||||
# The user is authenticated with a user not belonging to the requested organization dashboard
|
||||
# Let's get the first user in the session
|
||||
else
|
||||
organization_name = CartoDB::UserOrganization.user_belongs_to_organization?(user.username)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
render json: {
|
||||
urls: ["#{CartoDB.base_url(user.username, organization_name)}#{CartoDB.path(self, 'dashboard_bis')}"],
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
last_name: user.last_name,
|
||||
avatar_url: user.avatar_url,
|
||||
email: user.email,
|
||||
organization: Carto::Api::OrganizationPresenter.new(user.organization).to_poro,
|
||||
base_url: user.public_url
|
||||
}
|
||||
end
|
||||
|
||||
# TODO: this should be moved upwards in the controller hierarchy, and make it a replacement for current_user
|
||||
# URI present-user if has valid session, or nil
|
||||
def uri_user
|
||||
@uri_user ||= (current_user.nil? ? nil : Carto::User.where(id: current_user.id).first)
|
||||
end
|
||||
|
||||
# TODO: this should be moved upwards in the controller hierarchy, and make it a replacement for current_viewer
|
||||
# 1st user that has valid session, if coincides with URI then same as uri_user
|
||||
def session_user
|
||||
@session_user ||= (current_viewer.nil? ? nil : Carto::User.where(id: current_viewer.id).first)
|
||||
end
|
||||
|
||||
def update_user_attributes(user, attributes)
|
||||
update_password_if_needed(user, attributes)
|
||||
|
||||
if user.can_change_email? && attributes[:email].present?
|
||||
user.set_fields(attributes, [:email])
|
||||
end
|
||||
|
||||
if attributes[:avatar_url].present? && valid_avatar_file?(attributes[:avatar_url])
|
||||
user.set_fields(attributes, [:avatar_url])
|
||||
end
|
||||
|
||||
fields_to_be_updated = UPDATE_ME_FIELDS.select { |field| attributes.has_key?(field) }
|
||||
|
||||
user.set_fields(attributes, fields_to_be_updated) if fields_to_be_updated.present?
|
||||
end
|
||||
|
||||
def update_password_if_needed(user, attributes)
|
||||
if password_change?(user, attributes)
|
||||
user.change_password(
|
||||
attributes[:password_confirmation],
|
||||
attributes[:new_password],
|
||||
attributes[:confirm_password]
|
||||
)
|
||||
|
||||
update_session_security_token(user)
|
||||
end
|
||||
end
|
||||
|
||||
def password_change?(user, attributes)
|
||||
(attributes[:new_password].present? || attributes[:confirm_password].present?) && user.can_change_password?
|
||||
end
|
||||
|
||||
def recalculate_user_db_size
|
||||
current_user && Carto::UserDbSizeCache.new.update_if_old(current_user)
|
||||
end
|
||||
|
||||
def update_user_multifactor_authentication(user, mfa_enabled)
|
||||
return if mfa_enabled.nil?
|
||||
|
||||
service = Carto::UserMultifactorAuthUpdateService.new(user_id: user.id)
|
||||
service.update(enabled: mfa_enabled)
|
||||
warden.session(user.username)[:multifactor_authentication_performed] = false unless mfa_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
module Carto
|
||||
module Api
|
||||
class VisualizationExportPresenter
|
||||
|
||||
def initialize(visualization_export)
|
||||
@visualization_export = visualization_export
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return {} unless @visualization_export
|
||||
|
||||
{
|
||||
id: @visualization_export.id,
|
||||
visualization_id: @visualization_export.visualization_id,
|
||||
user_id: @visualization_export.user_id,
|
||||
state: @visualization_export.state,
|
||||
url: @visualization_export.url,
|
||||
created_at: @visualization_export.created_at,
|
||||
updated_at: @visualization_export.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
require_dependency 'carto/tracking/events'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class VisualizationExportsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
include VisualizationsControllerHelper
|
||||
|
||||
ssl_required :create, :show, :download
|
||||
|
||||
skip_before_filter :api_authorization_required, only: [:create, :show, :download]
|
||||
before_filter :optional_api_authorization, only: [:create, :show, :download]
|
||||
|
||||
before_filter :load_visualization, only: :create
|
||||
before_filter :load_visualization_export, only: [:show, :download]
|
||||
|
||||
rescue_from Carto::LoadError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UnauthorizedError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
|
||||
def create
|
||||
user = current_viewer ? Carto::User.find(current_viewer.id) : nil
|
||||
visualization_export = Carto::VisualizationExport.new(
|
||||
visualization: @visualization,
|
||||
user: user,
|
||||
user_tables_ids: params[:user_tables_ids])
|
||||
unless visualization_export.save
|
||||
if visualization_export.errors[:user].present?
|
||||
raise Carto::UnauthorizedError.new("Errors: #{visualization_export.errors.full_messages}")
|
||||
else
|
||||
CartoDB::Logger.warning(
|
||||
message: 'Validation error creating visualization export',
|
||||
user: user,
|
||||
visualization: @visualization.id,
|
||||
user_tables_ids: params[:user_tables_ids],
|
||||
visualization_export: visualization_export.inspect,
|
||||
error_messages: visualization_export.errors.full_messages
|
||||
)
|
||||
raise Carto::UnprocesableEntityError.new("Errors: #{visualization_export.errors.full_messages}")
|
||||
end
|
||||
end
|
||||
|
||||
# In order to generate paths you need to be in a controller, so path is sent to Resque
|
||||
download_path_params = { visualization_export_id: visualization_export.id }
|
||||
download_path = CartoDB.path(self, 'visualization_export_download', download_path_params)
|
||||
Resque.enqueue(Resque::ExporterJobs, job_id: visualization_export.id, download_path: download_path)
|
||||
|
||||
if current_viewer
|
||||
current_viewer_id = current_viewer.id
|
||||
Carto::Tracking::Events::ExportedMap.new(current_viewer_id,
|
||||
user_id: current_viewer_id,
|
||||
visualization_id: @visualization.id).report
|
||||
end
|
||||
|
||||
render_jsonp(VisualizationExportPresenter.new(visualization_export).to_poro, 201)
|
||||
end
|
||||
|
||||
def show
|
||||
render_jsonp(VisualizationExportPresenter.new(@visualization_export).to_poro, 200)
|
||||
end
|
||||
|
||||
def download
|
||||
send_file @visualization_export.file, type: "application/zip"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_visualization
|
||||
visualization_id = params[:visualization_id]
|
||||
@visualization = Carto::Visualization.where(id: visualization_id).first
|
||||
raise Carto::LoadError.new("Visualization not found: #{visualization_id}") unless @visualization
|
||||
end
|
||||
|
||||
def load_visualization_export
|
||||
id = params[:id].present? ? uuid_parameter(:id) : uuid_parameter(:visualization_export_id)
|
||||
@visualization_export = Carto::VisualizationExport.where(id: id).first
|
||||
raise Carto::LoadError.new("Visualization export not found: #{id}") unless @visualization_export
|
||||
export_user_id = @visualization_export.user_id
|
||||
unless export_user_id.nil? || (current_viewer && export_user_id == current_viewer.id)
|
||||
raise Carto::UnauthorizedError.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,280 @@
|
||||
require_relative 'external_source_presenter'
|
||||
require_relative 'permission_presenter'
|
||||
require_relative 'user_table_presenter'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class VisualizationPresenter
|
||||
|
||||
def initialize(visualization, current_viewer, context,
|
||||
related: true, related_canonical_visualizations: false, show_user: false,
|
||||
show_stats: true, show_table: true, show_liked: true,
|
||||
show_permission: true, show_synchronization: true, show_uses_builder_features: true,
|
||||
show_table_size_and_row_count: true, show_auth_tokens: true, show_user_basemaps: false,
|
||||
password: nil, with_dependent_visualizations: 0)
|
||||
@visualization = visualization
|
||||
@current_viewer = current_viewer
|
||||
@context = context
|
||||
|
||||
@related = related
|
||||
@load_related_canonical_visualizations = related_canonical_visualizations
|
||||
@show_user = show_user
|
||||
@show_stats = show_stats
|
||||
@show_liked = show_liked
|
||||
@show_table = show_table
|
||||
@show_permission = show_permission
|
||||
@show_synchronization = show_synchronization
|
||||
@show_uses_builder_features = show_uses_builder_features
|
||||
@show_table_size_and_row_count = show_table_size_and_row_count
|
||||
@show_auth_tokens = show_auth_tokens
|
||||
@show_user_basemaps = show_user_basemaps
|
||||
@password = password
|
||||
@with_dependent_visualizations = with_dependent_visualizations
|
||||
|
||||
@presenter_cache = Carto::Api::PresenterCache.new
|
||||
end
|
||||
|
||||
def with_presenter_cache(presenter_cache)
|
||||
@presenter_cache = presenter_cache
|
||||
self
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return_private_poro = @visualization.can_view_private_info?(@current_viewer)
|
||||
|
||||
poro = return_private_poro ? to_private_poro : to_public_poro
|
||||
|
||||
poro[:user] = user if show_user
|
||||
|
||||
if load_related_canonical_visualizations
|
||||
poro[:related_canonical_visualizations] = related_canonicals
|
||||
# The count doesn't take into account privacy concerns
|
||||
poro[:related_canonical_visualizations_count] = @visualization.related_canonical_visualizations.count
|
||||
end
|
||||
|
||||
if with_dependent_visualizations > 0
|
||||
dependencies = @visualization.dependent_visualizations
|
||||
poro[:dependent_visualizations_count] = dependencies.count
|
||||
limited_dependencies = most_recent_dependencies(dependencies, with_dependent_visualizations)
|
||||
poro[:dependent_visualizations] = limited_dependencies.map do |dependent_visualization|
|
||||
VisualizationPresenter.new(dependent_visualization, @current_viewer, @context).to_summarized_poro
|
||||
end
|
||||
end
|
||||
|
||||
poro[:liked] = @current_viewer ? @visualization.liked_by?(@current_viewer) : false if show_liked
|
||||
poro[:permission] = permission if show_permission
|
||||
poro[:stats] = show_stats ? @visualization.stats : {}
|
||||
|
||||
if show_auth_tokens
|
||||
poro[:auth_tokens] = auth_tokens
|
||||
elsif return_private_poro || @visualization.is_accessible_with_password?(@current_viewer, @password)
|
||||
poro[:auth_tokens] = auth_tokens
|
||||
end
|
||||
|
||||
poro[:table] = user_table_presentation if show_table
|
||||
|
||||
poro
|
||||
end
|
||||
|
||||
def to_private_poro
|
||||
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: @visualization.privacy.upcase,
|
||||
created_at: @visualization.created_at,
|
||||
updated_at: @visualization.updated_at,
|
||||
locked: @visualization.locked,
|
||||
source: @visualization.source,
|
||||
title: @visualization.title,
|
||||
license: @visualization.license,
|
||||
attributions: @visualization.attributions,
|
||||
kind: @visualization.kind,
|
||||
external_source: Carto::Api::ExternalSourcePresenter.new(@visualization.external_source).to_poro,
|
||||
url: url,
|
||||
version: @visualization.version || 2,
|
||||
# TODO: The following are Odyssey fields and could be removed
|
||||
# They are kept here for now for compatibility with the old presenter and JS code
|
||||
# `children` is hardcoded to avoid a performance impact (an extra query)
|
||||
prev_id: @visualization.prev_id,
|
||||
next_id: @visualization.next_id,
|
||||
parent_id: @visualization.parent_id,
|
||||
transition_options: @visualization.transition_options,
|
||||
active_child: nil,
|
||||
children: []
|
||||
}
|
||||
|
||||
poro[:related_tables] = related_tables if related
|
||||
poro[:synchronization] = synchronization if show_synchronization
|
||||
poro[:uses_builder_features] = @visualization.uses_builder_features? if show_uses_builder_features
|
||||
|
||||
poro
|
||||
end
|
||||
|
||||
def to_public_poro
|
||||
{
|
||||
id: @visualization.id,
|
||||
name: @visualization.name,
|
||||
display_name: @visualization.display_name,
|
||||
attributions: @visualization.attributions,
|
||||
source: @visualization.source,
|
||||
license: @visualization.license,
|
||||
type: @visualization.type,
|
||||
tags: @visualization.tags,
|
||||
description: @visualization.description,
|
||||
created_at: @visualization.created_at,
|
||||
updated_at: @visualization.updated_at,
|
||||
title: @visualization.title,
|
||||
kind: @visualization.kind,
|
||||
privacy: @visualization.privacy.upcase,
|
||||
}
|
||||
end
|
||||
|
||||
# For dependent visualizations
|
||||
def to_summarized_poro
|
||||
{
|
||||
id: @visualization.id,
|
||||
name: @visualization.name,
|
||||
updated_at: @visualization.updated_at,
|
||||
permission: permission.slice(:id, :owner),
|
||||
auth_tokens: auth_tokens
|
||||
}
|
||||
end
|
||||
|
||||
def to_search_preview_poro
|
||||
{
|
||||
type: @visualization.type,
|
||||
name: @visualization.name,
|
||||
url: url
|
||||
}
|
||||
end
|
||||
|
||||
# Ideally this should go at a lower level, as relates to url generation, but at least centralize logic here
|
||||
# INFO: For now, no support for non-org users, as intended use is for sharing urls
|
||||
def privacy_aware_map_url(additional_params = {}, action = 'public_visualizations_show_map')
|
||||
organization = @visualization.user.organization
|
||||
|
||||
return unless organization
|
||||
|
||||
# When a visualization is private, checks of permissions need not only the Id but also the vis owner database schema
|
||||
# Logic on public_map route will handle permissions so here we only "namespace the id" when proceeds
|
||||
if @visualization.is_privacy_private?
|
||||
# Final url will be like ORG.carto.com/u/VIEWER/viz/OWNER_SCHEMA.VIS_ID/public_map
|
||||
base_url_username = @current_viewer.username
|
||||
vis_id_schema = @visualization.user.database_schema
|
||||
else
|
||||
# Final url will be like ORG.carto.com/u/VIEWER/viz/VIS_ID/public_map
|
||||
base_url_username = @visualization.user.username
|
||||
vis_id_schema = nil
|
||||
end
|
||||
# this builds only the fragment /viz/xxxxx/public_map
|
||||
path = CartoDB.path(@context, action, additional_params.merge(id: qualified_visualization_id(vis_id_schema)))
|
||||
"#{CartoDB.base_url(organization.name, base_url_username)}#{path}"
|
||||
end
|
||||
|
||||
def qualified_visualization_id(schema = nil)
|
||||
schema.nil? ? @visualization.id : "#{schema}.#{@visualization.id}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :related, :load_related_canonical_visualizations, :show_user,
|
||||
:show_stats, :show_table, :show_liked,
|
||||
:show_permission, :show_synchronization, :show_uses_builder_features,
|
||||
:show_table_size_and_row_count, :show_auth_tokens,
|
||||
:show_user_basemaps, :with_dependent_visualizations
|
||||
|
||||
def user_table_presentation
|
||||
Carto::Api::UserTablePresenter.new(@visualization.user_table, @current_viewer,
|
||||
show_size_and_row_count: show_table_size_and_row_count)
|
||||
.with_presenter_cache(@presenter_cache).to_poro
|
||||
end
|
||||
|
||||
def synchronization
|
||||
Carto::Api::SynchronizationPresenter.new(@visualization.synchronization).to_poro
|
||||
end
|
||||
|
||||
def permission
|
||||
unless @visualization.permission.nil?
|
||||
Carto::Api::PermissionPresenter.new(@visualization.permission, current_viewer: @current_viewer)
|
||||
.with_presenter_cache(@presenter_cache).to_poro
|
||||
end
|
||||
end
|
||||
|
||||
def auth_tokens
|
||||
if @visualization.password_protected? && (@visualization.user.id == @current_viewer.try(:id) || @visualization.password_valid?(@password))
|
||||
@visualization.get_auth_tokens
|
||||
elsif @visualization.is_privacy_private?
|
||||
@current_viewer.get_auth_tokens
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def related_tables
|
||||
related = if @visualization.user_table
|
||||
@visualization.related_tables.select { |table| table.id != @visualization.user_table.id }
|
||||
else
|
||||
@visualization.related_tables
|
||||
end
|
||||
|
||||
related.map do |table|
|
||||
Carto::Api::UserTablePresenter.new(table, @current_viewer).with_presenter_cache(@presenter_cache).to_poro
|
||||
end
|
||||
end
|
||||
|
||||
def related_canonicals
|
||||
@visualization
|
||||
.related_canonical_visualizations
|
||||
.select { |v| v.is_viewable_by_user?(@current_viewer) }
|
||||
.map { |v| self.class.new(v, @current_viewer, @context).to_poro }
|
||||
end
|
||||
|
||||
def children_poro(visualization)
|
||||
{
|
||||
id: visualization.id,
|
||||
prev_id: visualization.prev_id,
|
||||
type: visualization.type,
|
||||
next_id: visualization.next_id,
|
||||
transition_options: visualization.transition_options,
|
||||
map_id: visualization.map_id
|
||||
}
|
||||
end
|
||||
|
||||
def url
|
||||
if @visualization.canonical?
|
||||
dataset_name = @visualization.qualified_name(@current_viewer).tr('"', '')
|
||||
CartoDB.url(@context, 'public_tables_show_bis',
|
||||
params: { id: dataset_name },
|
||||
user: @current_viewer)
|
||||
elsif @visualization.kuviz?
|
||||
CartoDB.url(@context, 'kuviz_show',
|
||||
params: { id: @visualization.id },
|
||||
user: @current_viewer)
|
||||
else
|
||||
CartoDB.url(@context, 'public_visualizations_show_map',
|
||||
params: { id: @visualization.id },
|
||||
user: @current_viewer)
|
||||
end
|
||||
end
|
||||
|
||||
def user
|
||||
Carto::Api::UserPresenter.new(@visualization.user,
|
||||
current_viewer: @current_viewer,
|
||||
fetch_db_size: false,
|
||||
fetch_basemaps: show_user_basemaps,
|
||||
fetch_profile: false).to_poro
|
||||
end
|
||||
|
||||
def most_recent_dependencies(dependencies, limit)
|
||||
sorted_dependencies = dependencies.sort_by(&:updated_at).reverse
|
||||
sorted_dependencies.first(limit)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,153 @@
|
||||
require_dependency 'carto/bounding_box_utils'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
module VisualizationSearcher
|
||||
|
||||
FILTER_SHARED_YES = 'yes'
|
||||
FILTER_SHARED_NO = 'no'
|
||||
FILTER_SHARED_ONLY = 'only'
|
||||
|
||||
# Creates a visualization query builder ready
|
||||
# to search based on params hash (can be request params).
|
||||
# It doesn't apply ordering or paging, just filtering.
|
||||
def query_builder_with_filter_from_hash(params)
|
||||
types, total_types = get_types_parameters
|
||||
|
||||
validate_parameters(types, params)
|
||||
|
||||
pattern = params[:q]
|
||||
|
||||
only_liked = params[:only_liked] == 'true'
|
||||
only_shared = params[:only_shared] == 'true'
|
||||
exclude_shared = params[:exclude_shared] == 'true'
|
||||
exclude_raster = params[:exclude_raster] == 'true'
|
||||
locked = params[:locked]
|
||||
shared = compose_shared(params[:shared], only_shared, exclude_shared)
|
||||
tags = params.fetch(:tags, '').split(',')
|
||||
tags = nil if tags.empty?
|
||||
bbox_parameter = params.fetch(:bbox,nil)
|
||||
privacy = params.fetch(:privacy,nil)
|
||||
only_with_display_name = params[:only_with_display_name] == 'true'
|
||||
with_dependent_visualizations = params[:with_dependent_visualizations].to_i
|
||||
only_published = params[:only_published] == 'true'
|
||||
|
||||
vqb = VisualizationQueryBuilder.new
|
||||
.with_prefetch_user
|
||||
.with_prefetch_table
|
||||
.with_prefetch_permission
|
||||
.with_prefetch_synchronization
|
||||
.with_prefetch_external_source
|
||||
.with_types(types)
|
||||
.with_tags(tags)
|
||||
|
||||
if !bbox_parameter.blank?
|
||||
vqb.with_bounding_box(Carto::BoundingBoxUtils.parse_bbox_parameters(bbox_parameter))
|
||||
end
|
||||
|
||||
# FIXME Patch to exclude legacy visualization from data-library #5097
|
||||
if only_with_display_name
|
||||
vqb.with_display_name
|
||||
end
|
||||
|
||||
vqb.with_published if only_published
|
||||
|
||||
if current_user
|
||||
vqb.with_current_user_id(current_user.id)
|
||||
vqb.with_liked_by_user_id(current_user.id) if only_liked
|
||||
case shared
|
||||
when FILTER_SHARED_YES
|
||||
vqb.with_owned_by_or_shared_with_user_id(current_user.id)
|
||||
when FILTER_SHARED_NO
|
||||
vqb.with_user_id(current_user.id) if !only_liked
|
||||
when FILTER_SHARED_ONLY
|
||||
vqb.with_shared_with_user_id(current_user.id)
|
||||
.with_user_id_not(current_user.id)
|
||||
end
|
||||
|
||||
vqb.without_raster if exclude_raster
|
||||
|
||||
if locked == 'true'
|
||||
vqb.with_locked(true)
|
||||
elsif locked == 'false'
|
||||
vqb.with_locked(false)
|
||||
end
|
||||
|
||||
if types.include? Carto::Visualization::TYPE_REMOTE
|
||||
vqb.without_synced_external_sources
|
||||
vqb.without_imported_remote_visualizations
|
||||
end
|
||||
|
||||
vqb.with_privacy(privacy) unless privacy.nil?
|
||||
|
||||
vqb.with_prefetch_dependent_visualizations if with_dependent_visualizations > 0
|
||||
|
||||
else
|
||||
# TODO: ok, this looks like business logic, refactor
|
||||
subdomain = CartoDB.extract_subdomain(request)
|
||||
vqb.with_user_id(Carto::User.where(username: subdomain).first.id)
|
||||
.with_privacy(Carto::Visualization::PRIVACY_PUBLIC)
|
||||
end
|
||||
|
||||
if pattern.present?
|
||||
vqb.with_partial_match(pattern)
|
||||
end
|
||||
|
||||
vqb
|
||||
end
|
||||
|
||||
def presenter_options_from_hash(params)
|
||||
options = {}
|
||||
options[:show_stats] = false if params[:show_stats].to_s == 'false'
|
||||
options[:show_table] = false if params[:show_table].to_s == 'false'
|
||||
options[:show_permission] = false if params[:show_permission].to_s == 'false'
|
||||
options[:show_uses_builder_features] = false if params[:show_uses_builder_features].to_s == 'false'
|
||||
options[:show_synchronization] = false if params[:show_synchronization].to_s == 'false'
|
||||
options[:show_table_size_and_row_count] = false if params[:show_table_size_and_row_count].to_s == 'false'
|
||||
options[:with_dependent_visualizations] = params[:with_dependent_visualizations].to_i
|
||||
options
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_types_parameters
|
||||
# INFO: this fits types and type into types, so only types is used for search.
|
||||
# types defaults to type if empty.
|
||||
# types defaults to derived if type is also empty.
|
||||
# total_types are the types used for total counts.
|
||||
types = params.fetch(:types, "").split(',')
|
||||
|
||||
type = params[:type].present? ? params[:type] : (types.empty? ? nil : types[0])
|
||||
# TODO: add this assumption to a test or remove it (this is coupled to the UI)
|
||||
total_types = [(type == Carto::Visualization::TYPE_REMOTE ? Carto::Visualization::TYPE_CANONICAL : type)].compact
|
||||
|
||||
types = [type].compact if types.empty?
|
||||
types = [Carto::Visualization::TYPE_DERIVED] if types.empty?
|
||||
|
||||
return types, total_types
|
||||
end
|
||||
|
||||
def compose_shared(shared, only_shared, exclude_shared)
|
||||
valid_shared = shared if [FILTER_SHARED_ONLY, FILTER_SHARED_NO, FILTER_SHARED_YES].include?(shared)
|
||||
return valid_shared if valid_shared
|
||||
|
||||
if only_shared
|
||||
FILTER_SHARED_ONLY
|
||||
elsif exclude_shared
|
||||
FILTER_SHARED_NO
|
||||
elsif exclude_shared == false
|
||||
FILTER_SHARED_YES
|
||||
else
|
||||
# INFO: exclude_shared == nil && !only_shared
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def validate_parameters(types, parameters)
|
||||
if (!params.fetch(:bbox, nil).nil? && (types.length > 1 || !types.include?(Carto::Visualization::TYPE_CANONICAL)))
|
||||
raise CartoDB::BoundingBoxError.new('Filter by bbox is only supported for type table')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
require_relative '../../../helpers/carto/html_safe'
|
||||
require_relative 'layer_vizjson_adapter'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class VisualizationVizJSONAdapter
|
||||
extend Forwardable
|
||||
include Carto::HtmlSafe
|
||||
|
||||
delegate [:id, :map, :qualified_name, :description, :retrieve_named_map?, :password_protected?, :overlays,
|
||||
:prev_id, :next_id, :transition_options, :has_password?, :parent_id, :get_auth_tokens, :user,
|
||||
:related_canonical_visualizations
|
||||
] => :visualization
|
||||
|
||||
attr_reader :visualization
|
||||
|
||||
def initialize(visualization, redis_cache = nil)
|
||||
@visualization = visualization
|
||||
@layer_cache = {}
|
||||
# INFO: needed for children
|
||||
@redis_cache = redis_cache
|
||||
end
|
||||
|
||||
def layers(kind)
|
||||
@layer_cache[kind] ||= get_layers(kind)
|
||||
end
|
||||
|
||||
def attributions_from_derived_visualizations
|
||||
@visualization.related_canonical_visualizations.map(&:attributions).reject {|attribution| attribution.blank?}
|
||||
end
|
||||
|
||||
def children
|
||||
@visualization.children.map { |v|
|
||||
Carto::Api::VisualizationVizJSONAdapter.new(v, @redis_cache)
|
||||
}
|
||||
end
|
||||
|
||||
def parent
|
||||
@visualization.parent ? Carto::Api::VisualizationVizJSONAdapter.new(@visualization.parent, @redis_cache) : nil
|
||||
end
|
||||
|
||||
def to_vizjson
|
||||
Carto::Api::VizJSONPresenter.new(self, @redis_cache).to_vizjson
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_layers(kind)
|
||||
choose_layers(kind).map { |layer|
|
||||
Carto::Api::LayerVizJSONAdapter.new(layer)
|
||||
}
|
||||
end
|
||||
|
||||
def choose_layers(kind)
|
||||
case kind
|
||||
when :base
|
||||
map.user_layers
|
||||
when :cartodb
|
||||
map.carto_layers
|
||||
when :others
|
||||
map.other_layers
|
||||
else
|
||||
raise "Unknown: #{kind}"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,508 @@
|
||||
require_relative 'visualization_presenter'
|
||||
require_dependency 'carto/api/vizjson_presenter'
|
||||
require_relative '../../../models/visualization/stats'
|
||||
require_relative 'paged_searcher'
|
||||
require_relative '../controller_helper'
|
||||
require_dependency 'carto/uuidhelper'
|
||||
require_dependency 'static_maps_url_helper'
|
||||
require_relative 'vizjson3_presenter'
|
||||
require_dependency 'visualization/name_generator'
|
||||
require_dependency 'visualization/table_blender'
|
||||
require_dependency 'carto/visualization_migrator'
|
||||
require_dependency 'carto/google_maps_api'
|
||||
require_dependency 'carto/ghost_tables_manager'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class VisualizationsController < ::Api::ApplicationController
|
||||
include VisualizationSearcher
|
||||
include PagedSearcher
|
||||
include Carto::UUIDHelper
|
||||
include Carto::ControllerHelper
|
||||
include VisualizationsControllerHelper
|
||||
include Carto::VisualizationMigrator
|
||||
|
||||
ssl_required :index, :show, :create, :update, :destroy, :google_maps_static_image
|
||||
ssl_allowed :vizjson2, :vizjson3, :list_watching, :static_map,
|
||||
:notify_watching, :list_watching, :add_like, :remove_like
|
||||
|
||||
# TODO: compare with older, there seems to be more optional authentication endpoints
|
||||
skip_before_filter :api_authorization_required, only: [:show, :index, :vizjson2, :vizjson3, :add_like,
|
||||
:remove_like, :notify_watching, :list_watching,
|
||||
:static_map, :show]
|
||||
|
||||
# :update and :destroy are correctly handled by permission check on the model
|
||||
before_filter :ensure_user_can_create, only: [:create]
|
||||
|
||||
before_filter :optional_api_authorization, only: [:show, :index, :vizjson2, :vizjson3, :add_like,
|
||||
:remove_like, :notify_watching, :list_watching, :static_map]
|
||||
|
||||
before_filter :id_and_schema_from_params
|
||||
|
||||
before_filter :load_visualization, only: [:add_like, :remove_like, :show,
|
||||
:list_watching, :notify_watching, :static_map, :vizjson2, :vizjson3,
|
||||
:update, :destroy, :google_maps_static_image]
|
||||
|
||||
before_filter :ensure_username_matches_visualization_owner, only: [:show, :static_map, :vizjson2, :vizjson3,
|
||||
:list_watching, :notify_watching, :update,
|
||||
:destroy, :google_maps_static_image]
|
||||
|
||||
before_filter :ensure_visualization_owned, only: [:destroy, :google_maps_static_image]
|
||||
before_filter :ensure_visualization_is_likeable, only: [:add_like, :remove_like]
|
||||
before_filter :link_ghost_tables, only: [:index]
|
||||
before_filter :load_common_data, only: [:index]
|
||||
|
||||
rescue_from Carto::LoadError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UnauthorizedError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UUIDParameterFormatError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::ProtectedVisualizationLoadError, with: :rescue_from_protected_visualization_load_error
|
||||
|
||||
VALID_ORDER_PARAMS = %i(name updated_at size mapviews favorited estimated_row_count privacy
|
||||
dependent_visualizations).freeze
|
||||
|
||||
def show
|
||||
presenter = VisualizationPresenter.new(
|
||||
@visualization, current_viewer, self,
|
||||
related_canonical_visualizations: params[:fetch_related_canonical_visualizations] == 'true',
|
||||
show_user: params[:fetch_user] == 'true',
|
||||
show_user_basemaps: params[:show_user_basemaps] == 'true',
|
||||
show_liked: params[:show_liked] == 'true',
|
||||
show_permission: params[:show_permission] == 'true',
|
||||
show_stats: params[:show_stats] == 'true',
|
||||
show_auth_tokens: params[:show_auth_tokens] == 'true',
|
||||
password: params[:password],
|
||||
with_dependent_visualizations: params[:with_dependent_visualizations].to_i || 0
|
||||
)
|
||||
|
||||
render_jsonp(::JSON.dump(presenter.to_poro))
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e)
|
||||
head(404)
|
||||
end
|
||||
|
||||
def index
|
||||
offdatabase_orders = Carto::VisualizationQueryOrderer::SUPPORTED_OFFDATABASE_ORDERS.map(&:to_sym)
|
||||
valid_order_combinations = VALID_ORDER_PARAMS - offdatabase_orders
|
||||
opts = { valid_order_combinations: valid_order_combinations }
|
||||
page, per_page, order, order_direction = page_per_page_order_params(VALID_ORDER_PARAMS, opts)
|
||||
_, total_types = get_types_parameters
|
||||
|
||||
vqb = query_builder_with_filter_from_hash(params)
|
||||
|
||||
presenter_cache = Carto::Api::PresenterCache.new
|
||||
presenter_options = presenter_options_from_hash(params).merge(related: false)
|
||||
|
||||
visualizations = vqb.with_order(order, order_direction)
|
||||
.build_paged(page, per_page).map do |v|
|
||||
VisualizationPresenter.new(v, current_viewer, self, presenter_options)
|
||||
.with_presenter_cache(presenter_cache).to_poro
|
||||
end
|
||||
response = {
|
||||
visualizations: visualizations,
|
||||
total_entries: vqb.build.size
|
||||
}
|
||||
if current_user && (params[:load_totals].to_s != 'false')
|
||||
response.merge!(calculate_totals(total_types))
|
||||
end
|
||||
render_jsonp(response)
|
||||
rescue CartoDB::BoundingBoxError => e
|
||||
render_jsonp({ error: e.message }, 400)
|
||||
rescue Carto::ParamInvalidError, Carto::ParamCombinationInvalidError => e
|
||||
render_jsonp({ error: e.message }, e.status)
|
||||
rescue StandardError => e
|
||||
CartoDB::Logger.error(exception: e)
|
||||
render_jsonp({ error: e.message }, 500)
|
||||
end
|
||||
|
||||
def add_like
|
||||
@visualization.add_like_from(current_viewer)
|
||||
render_jsonp(
|
||||
id: @visualization.id,
|
||||
liked: @visualization.liked_by?(current_viewer)
|
||||
)
|
||||
rescue Carto::Visualization::UnauthorizedLikeError
|
||||
render_jsonp({ text: "You don't have enough permissions to favorite this visualization" }, 403)
|
||||
rescue Carto::Visualization::AlreadyLikedError
|
||||
render_jsonp({ text: "You've already favorited this visualization" }, 400)
|
||||
end
|
||||
|
||||
def remove_like
|
||||
@visualization.remove_like_from(current_viewer)
|
||||
render_jsonp(id: @visualization.id, liked: @visualization.liked_by?(current_viewer))
|
||||
rescue Carto::Visualization::UnauthorizedLikeError
|
||||
render_jsonp({ text: "You don't have enough permissions to favorite this visualization" }, 403)
|
||||
end
|
||||
|
||||
def notify_watching
|
||||
return(head 403) unless @visualization.has_read_permission?(current_viewer)
|
||||
|
||||
watcher = Carto::Visualization::Watcher.new(current_user, @visualization)
|
||||
watcher.notify
|
||||
|
||||
render_jsonp(watcher.list)
|
||||
end
|
||||
|
||||
def list_watching
|
||||
return(head 403) unless @visualization.has_read_permission?(current_viewer)
|
||||
|
||||
render_jsonp(Carto::Visualization::Watcher.new(current_user, @visualization).list)
|
||||
end
|
||||
|
||||
def vizjson2
|
||||
@visualization.mark_as_vizjson2 unless carto_referer?
|
||||
render_vizjson(generate_vizjson2)
|
||||
end
|
||||
|
||||
def vizjson3
|
||||
render_vizjson(generate_vizjson3(@visualization))
|
||||
end
|
||||
|
||||
|
||||
def static_map
|
||||
# Abusing here of .to_i fallback to 0 if not a proper integer
|
||||
map_width = params.fetch('width',nil).to_i
|
||||
map_height = params.fetch('height', nil).to_i
|
||||
|
||||
# @see https://github.com/CartoDB/Windshaft-cartodb/blob/b59e0a00a04f822154c6d69acccabaf5c2fdf628/docs/Map-API.md#limits
|
||||
if map_width < 2 || map_height < 2 || map_width > 8192 || map_height > 8192
|
||||
return(head 400)
|
||||
end
|
||||
|
||||
response.headers['X-Cache-Channel'] = "#{@visualization.varnish_key}:vizjson"
|
||||
response.headers['Surrogate-Key'] = "#{CartoDB::SURROGATE_NAMESPACE_VIZJSON} #{@visualization.surrogate_key}"
|
||||
response.headers['Cache-Control'] = "max-age=86400,must-revalidate, public"
|
||||
|
||||
redirect_to Carto::StaticMapsURLHelper.new.url_for_static_map(request, @visualization, map_width, map_height)
|
||||
end
|
||||
|
||||
def create
|
||||
vis_data = payload
|
||||
|
||||
vis_data.delete(:permission)
|
||||
vis_data.delete(:permission_id)
|
||||
|
||||
param_tables = vis_data.delete(:tables)
|
||||
current_user_id = current_user.id
|
||||
|
||||
origin = 'blank'
|
||||
source_id = vis_data.delete(:source_visualization_id)
|
||||
valid_attributes = vis_data.slice(*VALID_CREATE_ATTRIBUTES)
|
||||
vis = if source_id
|
||||
user = Carto::User.find(current_user_id)
|
||||
source = Carto::Visualization.where(id: source_id).first
|
||||
return head(403) unless source && source.is_viewable_by_user?(user) && !source.kind_raster?
|
||||
if source.derived?
|
||||
origin = 'copy'
|
||||
duplicate_derived_visualization(source_id, user)
|
||||
else
|
||||
create_visualization_from_tables([source.user_table], valid_attributes)
|
||||
end
|
||||
elsif param_tables
|
||||
subdomain = CartoDB.extract_subdomain(request)
|
||||
viewed_user = Carto::User.where(username: subdomain).first
|
||||
tables = param_tables.map do |table_name|
|
||||
Carto::Helpers::TableLocator.new.get_by_id_or_name(table_name, viewed_user) if viewed_user
|
||||
end
|
||||
create_visualization_from_tables(tables.flatten, valid_attributes)
|
||||
else
|
||||
Carto::Visualization.new(valid_attributes.merge(name: name_candidate, user_id: current_user_id))
|
||||
end
|
||||
|
||||
vis.ensure_valid_privacy
|
||||
|
||||
vis.save!
|
||||
|
||||
current_viewer_id = current_viewer.id
|
||||
properties = {
|
||||
user_id: current_viewer_id,
|
||||
origin: origin,
|
||||
visualization_id: vis.id
|
||||
}
|
||||
|
||||
if vis.derived?
|
||||
Carto::Tracking::Events::CreatedMap.new(current_viewer_id, properties).report
|
||||
else
|
||||
Carto::Tracking::Events::CreatedDataset.new(current_viewer_id, properties).report
|
||||
end
|
||||
|
||||
render_jsonp(Carto::Api::VisualizationPresenter.new(vis, current_viewer, self).to_poro)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(message: "Error creating visualization", visualization_id: vis.try(:id), exception: e)
|
||||
raise e if e.is_a?(Carto::UnauthorizedError)
|
||||
render_jsonp({ errors: vis.try(:errors).try(:full_messages) }, 400)
|
||||
end
|
||||
|
||||
def update
|
||||
vis = @visualization
|
||||
|
||||
return head(403) unless payload[:id] == vis.id
|
||||
return head(403) unless vis.has_permission?(current_user, Carto::Permission::ACCESS_READWRITE)
|
||||
|
||||
vis_data = payload
|
||||
|
||||
vis_data.delete(:permission) || vis_data.delete('permission')
|
||||
vis_data.delete(:permission_id) || vis_data.delete('permission_id')
|
||||
|
||||
vis.transition_options = params[:transition_options] if params[:transition_options]
|
||||
|
||||
# when a table gets renamed, its canonical visualization is renamed, so we must revert renaming if that failed
|
||||
# This is far from perfect, but works without messing with table-vis sync and their two backends
|
||||
valid_attributes = vis_data.slice(*VALID_UPDATE_ATTRIBUTES)
|
||||
if vis.table?
|
||||
old_vis_name = vis.name
|
||||
|
||||
vis.attributes = valid_attributes
|
||||
new_vis_name = vis.name
|
||||
old_table_name = vis.table.name
|
||||
vis.save!
|
||||
if new_vis_name != old_vis_name && vis.table.name == old_table_name
|
||||
vis.name = old_vis_name
|
||||
vis.save!
|
||||
end
|
||||
else
|
||||
old_version = vis.version
|
||||
|
||||
vis.attributes = valid_attributes
|
||||
vis.save!
|
||||
|
||||
if version_needs_migration?(old_version, vis.version)
|
||||
migrate_visualization_to_v3(vis)
|
||||
end
|
||||
end
|
||||
|
||||
render_jsonp(Carto::Api::VisualizationPresenter.new(vis, current_viewer, self).to_poro)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(message: "Error updating visualization", visualization_id: vis.id, exception: e)
|
||||
error_code = vis.errors.include?(:privacy) ? 403 : 400
|
||||
render_jsonp({ errors: vis.errors.full_messages.empty? ? ['Error updating'] : vis.errors.full_messages },
|
||||
error_code)
|
||||
end
|
||||
|
||||
def destroy
|
||||
return head(403) unless @visualization.has_permission?(current_viewer, Carto::Permission::ACCESS_READWRITE)
|
||||
|
||||
current_viewer_id = current_viewer.id
|
||||
properties = { user_id: current_viewer_id, visualization_id: @visualization.id }
|
||||
|
||||
# Tracking. Can this be moved to the model?
|
||||
if @visualization.derived?
|
||||
Carto::Tracking::Events::DeletedMap.new(current_viewer_id, properties).report
|
||||
else
|
||||
Carto::Tracking::Events::DeletedDataset.new(current_viewer_id, properties).report
|
||||
end
|
||||
|
||||
if @visualization.table
|
||||
@visualization.table.fully_dependent_visualizations.each do |dependent_vis|
|
||||
properties = { user_id: current_viewer_id, visualization_id: dependent_vis.id }
|
||||
if dependent_vis.derived?
|
||||
Carto::Tracking::Events::DeletedMap.new(current_viewer_id, properties).report
|
||||
else
|
||||
Carto::Tracking::Events::DeletedDataset.new(current_viewer_id, properties).report
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@visualization.destroy
|
||||
|
||||
head 204
|
||||
rescue => exception
|
||||
CartoDB::Logger.error(message: 'Error deleting visualization', exception: exception,
|
||||
visualization: @visualization)
|
||||
render_jsonp({ errors: [exception.message] }, 400)
|
||||
end
|
||||
|
||||
def google_maps_static_image
|
||||
gmaps_api = Carto::GoogleMapsApi.new
|
||||
base_layer_options = @visualization.base_layers.first.options
|
||||
base_url = gmaps_api.build_static_image_url(
|
||||
center: params[:center],
|
||||
map_type: base_layer_options[:baseType],
|
||||
size: params[:size],
|
||||
zoom: params[:zoom],
|
||||
style: JSON.parse(base_layer_options[:style], symbolize_names: true)
|
||||
)
|
||||
|
||||
render(json: { url: gmaps_api.sign_url(@visualization.user, base_url) })
|
||||
rescue => e
|
||||
CartoDB::Logger.error(message: 'Error generating Google API URL', exception: e)
|
||||
render(json: { errors: 'Error generating static image URL' }, status: 400)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# excluded:
|
||||
# :id, :map_id, :type, :created_at, :external_source, :url, :version, :table, :user_id
|
||||
# :synchronization, :uses_builder_features, :auth_tokens, :transition_options, :prev_id, :next_id, :parent_id
|
||||
# :active_child, :permission
|
||||
VALID_UPDATE_ATTRIBUTES = [:name, :display_name, :active_layer_id, :tags, :description, :privacy, :updated_at,
|
||||
:locked, :source, :title, :license, :attributions, :kind, :password, :version].freeze
|
||||
# TODO: This lets more things through than it should. This is due to tests using this endpoint to create
|
||||
# test visualizations.
|
||||
VALID_CREATE_ATTRIBUTES = (VALID_UPDATE_ATTRIBUTES + [:type, :map_id] - [:version]).freeze
|
||||
|
||||
def generate_vizjson2
|
||||
Carto::Api::VizJSONPresenter.new(@visualization, $tables_metadata).to_vizjson(https_request: is_https?)
|
||||
end
|
||||
|
||||
def render_vizjson(vizjson)
|
||||
set_vizjson_response_headers_for(@visualization)
|
||||
render_jsonp(vizjson)
|
||||
rescue => exception
|
||||
CartoDB.notify_exception(exception)
|
||||
raise exception
|
||||
end
|
||||
|
||||
def load_visualization
|
||||
@visualization = load_visualization_from_id_or_name(params[:id])
|
||||
|
||||
if @visualization.nil?
|
||||
raise Carto::LoadError.new('Visualization does not exist', 404)
|
||||
end
|
||||
|
||||
if !@visualization.is_accessible_with_password?(current_viewer, params[:password])
|
||||
if @visualization.password_protected?
|
||||
raise Carto::ProtectedVisualizationLoadError.new(@visualization)
|
||||
else
|
||||
raise Carto::LoadError.new('Visualization not viewable', 403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_username_matches_visualization_owner
|
||||
unless request_username_matches_visualization_owner
|
||||
raise Carto::LoadError.new('Visualization of that user does not exist', 404)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_visualization_owned
|
||||
raise Carto::LoadError.new('Visualization not editable', 403) unless @visualization.is_owner?(current_viewer)
|
||||
end
|
||||
|
||||
def ensure_visualization_is_likeable
|
||||
return(head 403) unless current_viewer && @visualization.is_viewable_by_user?(current_viewer)
|
||||
end
|
||||
|
||||
def ensure_user_can_create
|
||||
return (head 403) unless current_viewer && !current_viewer.viewer
|
||||
end
|
||||
|
||||
# This avoids crossing usernames and visualizations.
|
||||
# Remember that the url of a visualization shared with a user contains that user's username instead of owner's
|
||||
def request_username_matches_visualization_owner
|
||||
# Support both for username at `/u/username` and subdomain, prioritizing first
|
||||
username = [CartoDB.username_from_request(request), CartoDB.subdomain_from_request(request)].compact.first
|
||||
# URL must always contain username, either at subdomain or at path.
|
||||
# Domainless url documentation: http://cartodb.readthedocs.org/en/latest/configuration.html#domainless-urls
|
||||
return false unless username.present?
|
||||
|
||||
# Either user is owner or is current and has permission
|
||||
# R permission check is based on current_viewer because current_user assumes you're viewing your subdomain
|
||||
username == @visualization.user.username ||
|
||||
(current_user && username == current_user.username && @visualization.has_read_permission?(current_viewer))
|
||||
end
|
||||
|
||||
def id_and_schema_from_params
|
||||
if params.fetch('id', nil) =~ /\./
|
||||
@id, @schema = params.fetch('id').split('.').reverse
|
||||
else
|
||||
@id, @schema = [params.fetch('id', nil), nil]
|
||||
end
|
||||
end
|
||||
|
||||
def set_vizjson_response_headers_for(visualization)
|
||||
# We don't cache non-public vis
|
||||
if @visualization.is_publically_accesible?
|
||||
response.headers['X-Cache-Channel'] = "#{@visualization.varnish_key}:vizjson"
|
||||
response.headers['Surrogate-Key'] = "#{CartoDB::SURROGATE_NAMESPACE_VIZJSON} #{visualization.surrogate_key}"
|
||||
response.headers['Cache-Control'] = 'no-cache,max-age=86400,must-revalidate, public'
|
||||
end
|
||||
end
|
||||
|
||||
def carto_referer?
|
||||
referer_host = URI.parse(request.referer).host
|
||||
referer_host && (referer_host.ends_with?('carto.com') || referer_host.ends_with?('cartodb.com'))
|
||||
rescue URI::InvalidURIError
|
||||
false
|
||||
end
|
||||
|
||||
def payload
|
||||
request.body.rewind
|
||||
::JSON.parse(request.body.read.to_s || String.new, symbolize_names: true)
|
||||
end
|
||||
|
||||
def duplicate_derived_visualization(source, user)
|
||||
export_service = Carto::VisualizationsExportService2.new
|
||||
visualization_hash = export_service.export_visualization_json_hash(source, user)
|
||||
visualization_copy = export_service.build_visualization_from_hash_export(visualization_hash)
|
||||
visualization_copy.name = name_candidate
|
||||
visualization_copy.version = user.new_visualizations_version
|
||||
Carto::VisualizationsExportPersistenceService.new.save_import(user, visualization_copy)
|
||||
|
||||
visualization_copy
|
||||
end
|
||||
|
||||
def name_candidate
|
||||
CartoDB::Visualization::NameGenerator.new(current_user).name(params[:name])
|
||||
end
|
||||
|
||||
def create_visualization_from_tables(tables, vis_data)
|
||||
blender = CartoDB::Visualization::TableBlender.new(Carto::User.find(current_user.id), tables)
|
||||
map = blender.blend
|
||||
|
||||
Carto::Visualization.new(vis_data.merge(name: name_candidate,
|
||||
map_id: map.id,
|
||||
type: 'derived',
|
||||
privacy: blender.blended_privacy,
|
||||
user_id: current_user.id,
|
||||
overlays: Carto::OverlayFactory.build_default_overlays(current_user)))
|
||||
end
|
||||
|
||||
def link_ghost_tables
|
||||
return unless current_user && current_user.has_feature_flag?('ghost_tables')
|
||||
|
||||
# This call will trigger ghost tables synchronously if there's risk of displaying a stale table
|
||||
# or asynchronously otherwise.
|
||||
Carto::GhostTablesManager.new(current_user.id).link_ghost_tables
|
||||
end
|
||||
|
||||
def load_common_data
|
||||
return true unless current_user.present?
|
||||
begin
|
||||
visualizations_api_url = CartoDB::Visualization::CommonDataService.build_url(self)
|
||||
::Resque.enqueue(::Resque::UserDBJobs::CommonData::LoadCommonData, current_user.id, visualizations_api_url) if current_user.should_load_common_data?
|
||||
rescue Exception => e
|
||||
# We don't block the load of the dashboard because we aren't able to load common dat
|
||||
CartoDB.notify_exception(e, {user:current_user})
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_totals(total_types)
|
||||
# Prefetching at counts removes duplicates
|
||||
{
|
||||
total_user_entries: VisualizationQueryBuilder.new
|
||||
.with_types(total_types)
|
||||
.with_user_id(current_user.id)
|
||||
.build.size,
|
||||
total_locked: VisualizationQueryBuilder.new
|
||||
.with_types(total_types)
|
||||
.with_user_id(current_user.id)
|
||||
.with_locked(true)
|
||||
.build.size,
|
||||
total_likes: VisualizationQueryBuilder.new
|
||||
.with_types(total_types)
|
||||
.with_liked_by_user_id(current_user.id)
|
||||
.with_locked(false)
|
||||
.build.size,
|
||||
total_shared: VisualizationQueryBuilder.new
|
||||
.with_types(total_types)
|
||||
.with_shared_with_user_id(current_user.id)
|
||||
.with_user_id_not(current_user.id)
|
||||
.with_locked(false)
|
||||
.with_prefetch_table
|
||||
.build.size
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,445 @@
|
||||
require_dependency 'carto/api/layer_vizjson_adapter'
|
||||
require_dependency 'carto/api/infowindow_migrator'
|
||||
require_dependency 'cartodb/redis_vizjson_cache'
|
||||
require_dependency 'carto/named_maps/template'
|
||||
require_dependency 'carto/legend_migrator'
|
||||
require_dependency 'carto/html_safe'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
module ApiTemplates
|
||||
API_TEMPLATE_PUBLIC = 'public'.freeze
|
||||
API_TEMPLATE_PRIVATE = 'private'.freeze
|
||||
|
||||
def api_templates_type(options)
|
||||
options.fetch(:https_request, false) ? API_TEMPLATE_PRIVATE : API_TEMPLATE_PUBLIC
|
||||
end
|
||||
end
|
||||
|
||||
module DisplayVizjsonMode
|
||||
def display_named_map?(visualization, forced_privacy_version)
|
||||
forced_privacy_version != :force_anonymous && (visualization.retrieve_named_map? || forced_privacy_version == :force_named)
|
||||
end
|
||||
end
|
||||
|
||||
class VizJSON3Presenter
|
||||
include ApiTemplates
|
||||
include DisplayVizjsonMode
|
||||
include Carto::HtmlSafe
|
||||
|
||||
def initialize(visualization,
|
||||
redis_vizjson_cache = CartoDB::Visualization::RedisVizjsonCache.new($tables_metadata, 3))
|
||||
@visualization = visualization
|
||||
@redis_vizjson_cache = redis_vizjson_cache
|
||||
end
|
||||
|
||||
def to_vizjson(https_request: false)
|
||||
generate_vizjson(https_request: https_request, forced_privacy_version: nil)
|
||||
end
|
||||
|
||||
def to_named_map_vizjson(https_request: false)
|
||||
generate_vizjson(https_request: https_request, forced_privacy_version: :force_named)
|
||||
end
|
||||
|
||||
def to_anonymous_map_vizjson(https_request: false)
|
||||
generate_vizjson(https_request: https_request, forced_privacy_version: :force_anonymous)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_vizjson(https_request:, forced_privacy_version:)
|
||||
https_request ||= false
|
||||
version = case forced_privacy_version
|
||||
when :force_named
|
||||
'3n'
|
||||
when :force_anonymous
|
||||
'3a'
|
||||
else
|
||||
3
|
||||
end
|
||||
|
||||
vizjson = if @redis_vizjson_cache
|
||||
@redis_vizjson_cache.cached(@visualization.id, https_request, version) do
|
||||
calculate_vizjson(https_request: https_request, forced_privacy_version: forced_privacy_version)
|
||||
end
|
||||
else
|
||||
calculate_vizjson(https_request: https_request, forced_privacy_version: forced_privacy_version)
|
||||
end
|
||||
|
||||
vizjson
|
||||
end
|
||||
|
||||
VIZJSON_VERSION = '3.0.0'.freeze
|
||||
|
||||
def calculate_vizjson(https_request: false, forced_privacy_version: nil)
|
||||
options = { https_request: https_request }
|
||||
|
||||
user = @visualization.user
|
||||
map = @visualization.map
|
||||
|
||||
vizjson = {
|
||||
bounds: bounds_from(map),
|
||||
center: map.center,
|
||||
datasource: datasource_vizjson(options, forced_privacy_version),
|
||||
description: markdown_html_safe(@visualization.description),
|
||||
options: map_options(@visualization),
|
||||
id: @visualization.id,
|
||||
layers: layers_vizjson(forced_privacy_version),
|
||||
map_provider: map.provider,
|
||||
overlays: overlays_vizjson(@visualization),
|
||||
title: @visualization.qualified_name(user),
|
||||
updated_at: @visualization.updated_at,
|
||||
user: user_info_vizjson(user),
|
||||
version: VIZJSON_VERSION,
|
||||
widgets: widgets_vizjson,
|
||||
zoom: map.zoom
|
||||
}
|
||||
|
||||
visualization_analyses = @visualization.analyses
|
||||
vizjson[:analyses] = if display_named_map?(@visualization, forced_privacy_version)
|
||||
visualization_analyses.map { |a| named_map_analysis_json(a) }
|
||||
else
|
||||
visualization_analyses.map(&:analysis_definition)
|
||||
end
|
||||
|
||||
vizjson
|
||||
end
|
||||
|
||||
def bounds_from(map)
|
||||
return nil unless map.view_bounds_sw && map.view_bounds_ne
|
||||
|
||||
::JSON.parse("[#{map.view_bounds_sw}, #{map.view_bounds_ne}]")
|
||||
rescue => e
|
||||
CartoDB::Logger.debug(
|
||||
message: "Error parsing map bounds: #{map.id}, #{map.view_bounds_sw}, #{map.view_bounds_ne}", exception: e)
|
||||
nil
|
||||
end
|
||||
|
||||
def layers_vizjson(forced_privacy_version)
|
||||
ordered_layers = (
|
||||
[basemap_layer] +
|
||||
@visualization.carto_layers +
|
||||
@visualization.other_layers +
|
||||
non_basemap_base_layers).compact
|
||||
|
||||
if display_named_map?(@visualization, forced_privacy_version)
|
||||
named_map_vizjson_for_layers(ordered_layers)
|
||||
else
|
||||
anonymous_vizjson_for_layers(ordered_layers)
|
||||
end
|
||||
end
|
||||
|
||||
def named_map_vizjson_for_layers(layers)
|
||||
layers.map do |layer|
|
||||
VizJSON3NamedMapLayerPresenter.new(layer, configuration).to_vizjson
|
||||
end
|
||||
end
|
||||
|
||||
def anonymous_vizjson_for_layers(layers)
|
||||
layers.map do |layer|
|
||||
VizJSON3LayerPresenter.new(layer, configuration).to_vizjson
|
||||
end
|
||||
end
|
||||
|
||||
def basemap_layer
|
||||
@visualization.user_layers.first
|
||||
end
|
||||
|
||||
def non_basemap_base_layers
|
||||
base_layers = @visualization.user_layers
|
||||
base_layers.empty? ? [] : base_layers.slice(1, base_layers.length)
|
||||
end
|
||||
|
||||
def named_map_analysis_json(analysis)
|
||||
analysis_definition_without_sources(analysis.analysis_definition)
|
||||
end
|
||||
|
||||
def analysis_definition_without_sources(analysis_definition)
|
||||
if analysis_definition[:type] == 'source'
|
||||
analysis_definition.delete(:params)
|
||||
elsif analysis_definition[:params] && analysis_definition[:params][:source]
|
||||
analysis_definition_without_sources(analysis_definition[:params][:source])
|
||||
end
|
||||
analysis_definition
|
||||
end
|
||||
|
||||
def configuration
|
||||
Cartodb.config
|
||||
end
|
||||
|
||||
def datasource_vizjson(options, forced_privacy_version)
|
||||
ds = {
|
||||
user_name: @visualization.user.username,
|
||||
maps_api_template: ApplicationHelper.maps_api_template(api_templates_type(options)),
|
||||
stat_tag: @visualization.id
|
||||
}
|
||||
|
||||
if display_named_map?(@visualization, forced_privacy_version)
|
||||
ds[:template_name] = Carto::NamedMaps::Template.new(@visualization).name
|
||||
end
|
||||
|
||||
ds
|
||||
end
|
||||
|
||||
def user_info_vizjson(user)
|
||||
{
|
||||
fullname: user.name_or_username,
|
||||
avatar_url: user.avatar_url,
|
||||
profile_url: user.public_url
|
||||
}
|
||||
end
|
||||
|
||||
def widgets_vizjson
|
||||
@visualization.widgets.map do |widget|
|
||||
Carto::Api::WidgetPresenter.new(widget).to_vizjson
|
||||
end
|
||||
end
|
||||
|
||||
def map_options(visualization)
|
||||
map = visualization.map
|
||||
|
||||
migration_options = { layer_selector: true } if visualization.overlays.any? { |o| o.type == 'layer_selector' }
|
||||
|
||||
map.options.merge(migration_options || {})
|
||||
end
|
||||
|
||||
def overlays_vizjson(visualization)
|
||||
visualization.overlays.
|
||||
reject { |o| o.type == 'layer_selector' }.
|
||||
map { |o| Carto::Api::OverlayPresenter.new(o).to_vizjson }
|
||||
end
|
||||
end
|
||||
|
||||
class VizJSON3NamedMapLayerPresenter
|
||||
include ApiTemplates
|
||||
|
||||
def initialize(layer, configuration)
|
||||
@configuration = configuration
|
||||
@layer = layer
|
||||
end
|
||||
|
||||
# Prepare a PORO (Hash object) for easy JSONification
|
||||
# @see https://github.com/CartoDB/carto.js/blob/privacy-maps/doc/vizjson_format.md
|
||||
def to_vizjson
|
||||
layer_vizjson = VizJSON3LayerPresenter.new(@layer, @configuration).to_vizjson
|
||||
if @layer.user_layer?
|
||||
layer_vizjson
|
||||
elsif layer_vizjson[:type] == 'torque'
|
||||
data_for_torque_layer(layer_vizjson)
|
||||
else
|
||||
data_for_carto_layer(layer_vizjson)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def data_for_torque_layer(layer_vizjson)
|
||||
layer_vizjson.delete(:sql)
|
||||
layer_vizjson
|
||||
end
|
||||
|
||||
def data_for_carto_layer(layer_vizjson)
|
||||
layer_options = layer_vizjson[:options]
|
||||
data = {
|
||||
id: layer_vizjson[:id],
|
||||
type: layer_vizjson[:type],
|
||||
visible: layer_vizjson[:visible],
|
||||
options: {
|
||||
layer_name: layer_options[:layer_name],
|
||||
attribution: layer_options[:attribution],
|
||||
cartocss: layer_options[:cartocss]
|
||||
}
|
||||
}
|
||||
|
||||
if layer_options && layer_options[:source]
|
||||
data[:options][:source] = layer_options[:source]
|
||||
end
|
||||
|
||||
infowindow = layer_vizjson[:infowindow]
|
||||
if infowindow && infowindow['fields'] && !infowindow['fields'].empty?
|
||||
data[:infowindow] = infowindow
|
||||
end
|
||||
|
||||
tooltip = layer_vizjson[:tooltip]
|
||||
if tooltip && tooltip['fields'] && !tooltip['fields'].empty?
|
||||
data[:tooltip] = tooltip
|
||||
end
|
||||
|
||||
legend = layer_vizjson[:legend]
|
||||
if legend && legend['type'] != 'none'
|
||||
data[:legend] = legend
|
||||
end
|
||||
|
||||
data[:legends] = layer_vizjson[:legends] || []
|
||||
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
class VizJSON3LayerPresenter
|
||||
include ApiTemplates
|
||||
include InfowindowMigrator
|
||||
|
||||
EMPTY_CSS = '#dummy{}'.freeze
|
||||
|
||||
TORQUE_ATTRS = %i(
|
||||
table_name
|
||||
user_name
|
||||
property
|
||||
blendmode
|
||||
resolution
|
||||
countby
|
||||
torque-duration
|
||||
torque-steps
|
||||
torque-blend-mode
|
||||
named_map
|
||||
visible
|
||||
attribution
|
||||
).freeze
|
||||
|
||||
INFOWINDOW_AND_TOOLTIP_KEYS = %w(
|
||||
fields template_name template template_type alternative_names width maxHeight headerColor
|
||||
).freeze
|
||||
|
||||
def initialize(layer, configuration = {})
|
||||
@layer = layer
|
||||
@configuration = configuration
|
||||
end
|
||||
|
||||
def to_vizjson
|
||||
vizjson = { id: @layer.id }
|
||||
|
||||
if @layer.base_layer?
|
||||
vizjson.merge(as_base)
|
||||
else
|
||||
vizjson.merge(as_data).merge(@layer.torque? ? as_torque : as_carto)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attribution
|
||||
@layer.user_tables.map(&:visualization).map(&:attributions).join(', ')
|
||||
end
|
||||
|
||||
def as_data
|
||||
old_legend_not_migrated = @layer.legend && !@layer.legend[:migrated]
|
||||
|
||||
if @layer.persisted? && old_legend_not_migrated
|
||||
@layer.legends.any? ? mark_old_legend_migrated : migrate_old_legend
|
||||
end
|
||||
|
||||
legends_presentation = @layer.legends.map do |legend|
|
||||
Carto::Api::LegendPresenter.new(legend).to_hash
|
||||
end
|
||||
|
||||
{ legends: legends_presentation }
|
||||
end
|
||||
|
||||
def migrate_old_legend
|
||||
Carto::LegendMigrator.new(@layer.id, @layer.legend).build.save
|
||||
mark_old_legend_migrated
|
||||
@layer.legends.reload
|
||||
end
|
||||
|
||||
def mark_old_legend_migrated
|
||||
@layer.options[:legend][:migrated] = true
|
||||
@layer.save
|
||||
end
|
||||
|
||||
def as_torque
|
||||
layer_options = @layer.options.deep_symbolize_keys
|
||||
torque_options = layer_options.select { |k| TORQUE_ATTRS.include? k }
|
||||
torque_options[:attribution] = attribution
|
||||
torque_options[:layer_name] = layer_name
|
||||
|
||||
torque = {
|
||||
type: 'torque',
|
||||
options: torque_options
|
||||
}
|
||||
|
||||
torque[:cartocss] = layer_options[:tile_style]
|
||||
torque[:cartocss_version] = layer_options[:style_version]
|
||||
torque[:sql] = sql_from(@layer)
|
||||
|
||||
if @layer.options['source'].present?
|
||||
torque[:source] = @layer.options['source']
|
||||
end
|
||||
|
||||
sql_wrap = @layer.options['sql_wrap'] || @layer.options['query_wrapper']
|
||||
torque[:sql_wrap] = sql_wrap if sql_wrap
|
||||
|
||||
torque
|
||||
end
|
||||
|
||||
def as_carto
|
||||
{
|
||||
type: 'CartoDB',
|
||||
infowindow: whitelisted_attrs(migrate_builder_infowindow(@layer.infowindow, mustache_dir: 'infowindows')),
|
||||
tooltip: whitelisted_attrs(migrate_builder_infowindow(@layer.tooltip, mustache_dir: 'tooltips')),
|
||||
visible: @layer.options['visible'],
|
||||
options: options_data
|
||||
}
|
||||
end
|
||||
|
||||
def as_base
|
||||
{
|
||||
type: @layer.kind,
|
||||
options: @layer.options
|
||||
}
|
||||
end
|
||||
|
||||
def options_data
|
||||
data = {
|
||||
layer_name: layer_name,
|
||||
cartocss: css_from(@layer.options),
|
||||
cartocss_version: @layer.options.fetch('style_version'),
|
||||
attribution: attribution
|
||||
}
|
||||
source = @layer.options['source']
|
||||
if source
|
||||
data[:source] = source
|
||||
data.delete(:sql)
|
||||
else
|
||||
data[:sql] = sql_from(@layer)
|
||||
end
|
||||
|
||||
sql_wrap = @layer.options['sql_wrap'] || @layer.options['query_wrapper']
|
||||
data[:sql_wrap] = sql_wrap if sql_wrap
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
def layer_name
|
||||
layer_alias = @layer.options.fetch('table_name_alias', nil)
|
||||
table_name = @layer.options.fetch('table_name')
|
||||
|
||||
# .present? is not valid, as we want to allow whitespaced layer names.
|
||||
layer_alias.nil? || layer_alias.empty? ? table_name : layer_alias
|
||||
end
|
||||
|
||||
def sql_from(layer)
|
||||
query = layer.options.fetch('query', '')
|
||||
query.present? ? query : layer.default_query(layer.visualization.user)
|
||||
end
|
||||
|
||||
def css_from(options)
|
||||
style = options.include?('tile_style') ? options['tile_style'] : nil
|
||||
(style.nil? || style.strip.empty?) ? EMPTY_CSS : options.fetch('tile_style')
|
||||
end
|
||||
|
||||
def public_options
|
||||
return @configuration if @configuration.empty?
|
||||
@configuration.fetch(:layer_opts).fetch("public_opts")
|
||||
end
|
||||
|
||||
def whitelisted_attrs(infowindow_or_tooltip)
|
||||
if infowindow_or_tooltip
|
||||
infowindow_or_tooltip.select do |key, _|
|
||||
INFOWINDOW_AND_TOOLTIP_KEYS.include?(key) || INFOWINDOW_AND_TOOLTIP_KEYS.include?(key.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
require_dependency 'visualization/vizjson'
|
||||
require_dependency 'carto/api/visualization_vizjson_adapter'
|
||||
require_dependency 'cartodb/redis_vizjson_cache'
|
||||
|
||||
module Carto
|
||||
module Api
|
||||
class VizJSONPresenter
|
||||
|
||||
def initialize(visualization, redis_cache)
|
||||
@visualization = visualization
|
||||
@redis_cache = redis_cache
|
||||
@redis_vizjson_cache = CartoDB::Visualization::RedisVizjsonCache.new(redis_cache)
|
||||
end
|
||||
|
||||
def to_vizjson(options={})
|
||||
@redis_vizjson_cache.cached(@visualization.id, options.fetch(:https_request, false)) do
|
||||
calculate_vizjson(options)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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)
|
||||
CartoDB::Visualization::VizJSON.new(Carto::Api::VisualizationVizJSONAdapter.new(@visualization, @redis_cache), vizjson_options, Cartodb.config).to_poro
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= @visualization.user
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
module Carto
|
||||
module Api
|
||||
class WidgetPresenter
|
||||
|
||||
def initialize(widget)
|
||||
@widget = widget
|
||||
end
|
||||
|
||||
def to_poro
|
||||
return {} unless @widget
|
||||
|
||||
poro = {
|
||||
id: @widget.id,
|
||||
type: @widget.type,
|
||||
title: @widget.title,
|
||||
order: @widget.order,
|
||||
layer_id: @widget.layer_id,
|
||||
options: @widget.options,
|
||||
style: @widget.style
|
||||
}
|
||||
|
||||
poro[:source] = { id: @widget.source_id } if @widget.source_id.present?
|
||||
|
||||
poro
|
||||
end
|
||||
|
||||
alias_method :to_vizjson, :to_poro
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,148 @@
|
||||
module Carto
|
||||
module Api
|
||||
class WidgetsController < ::Api::ApplicationController
|
||||
include Carto::ControllerHelper
|
||||
|
||||
ssl_required :show, :create, :update, :destroy, :update_many
|
||||
|
||||
before_filter :load_map
|
||||
before_filter :load_layer, :load_widget_id, except: [:update_many]
|
||||
before_filter :load_widget, only: [:show, :update, :destroy]
|
||||
|
||||
rescue_from Carto::LoadError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UnauthorizedError, with: :rescue_from_carto_error
|
||||
rescue_from Carto::UnprocesableEntityError, with: :rescue_from_carto_error
|
||||
|
||||
def show
|
||||
render_jsonp(WidgetPresenter.new(@widget).to_poro)
|
||||
end
|
||||
|
||||
def create
|
||||
order_param = params[:order]
|
||||
order = if order_param.present?
|
||||
order_param
|
||||
else
|
||||
@map.visualization.widgets.count
|
||||
end
|
||||
widget = Carto::Widget.new(
|
||||
layer_id: @layer_id,
|
||||
order: order,
|
||||
type: params[:type],
|
||||
title: params[:title],
|
||||
options: params[:options],
|
||||
style: params[:style],
|
||||
source_id: source_id_from_params)
|
||||
widget.save!
|
||||
|
||||
Carto::Tracking::Events::CreatedWidget.new(current_viewer.id,
|
||||
user_id: current_viewer.id,
|
||||
visualization_id: @layer.visualization.id,
|
||||
widget_id: widget.id).report
|
||||
|
||||
render_jsonp(WidgetPresenter.new(widget).to_poro, 201)
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
render json: { errors: widget.errors }, status: 422
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e, message: "Error creating widget", widget: (widget ? widget : 'not created'))
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
def update
|
||||
update_widget!(@widget, params)
|
||||
|
||||
render_jsonp(WidgetPresenter.new(@widget).to_poro)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e, message: "Error updating widget", widget: @widget)
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
def update_many
|
||||
entries = params[:_json].map do |json_widget|
|
||||
{ widget: widget_with_validations(json_widget[:id]), json: json_widget }
|
||||
end
|
||||
result = ActiveRecord::Base.transaction do
|
||||
entries.map { |entry| update_widget!(entry[:widget], entry[:json]) }
|
||||
end
|
||||
render_jsonp(result.map { |widget| WidgetPresenter.new(widget).to_poro })
|
||||
end
|
||||
|
||||
def destroy
|
||||
@widget.destroy
|
||||
|
||||
render_jsonp(WidgetPresenter.new(@widget).to_poro)
|
||||
rescue => e
|
||||
CartoDB::Logger.error(exception: e, message: "Error destroying widget", widget: @widget)
|
||||
render json: { errors: e.message }, status: 500
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_widget!(widget, json_params)
|
||||
update_params = json_params.permit(:order, :type, :title)
|
||||
update_params[:source_id] = source_id_from_params(json_params) if source_id_from_params(json_params)
|
||||
widget.update_attributes(update_params)
|
||||
widget.options = json_params[:options]
|
||||
widget.style = json_params[:style]
|
||||
widget.save!
|
||||
widget
|
||||
end
|
||||
|
||||
def load_map
|
||||
@map_id = params[:map_id]
|
||||
@map = Carto::Map.where(id: @map_id).first
|
||||
raise LoadError.new("Map not found: #{@map_id}") unless @map
|
||||
raise Carto::UnauthorizedError.new("Not authorized for map #{@map.id}") unless @map.writable_by_user?(current_user)
|
||||
end
|
||||
|
||||
def load_layer
|
||||
@layer_id = params[:map_layer_id]
|
||||
payload_layer_id = params['layer_id']
|
||||
|
||||
if [payload_layer_id, @layer_id].compact.uniq.length >= 2
|
||||
raise UnprocesableEntityError.new("URL layer id (#{@layer_id}) and payload layer id (#{payload_layer_id}) don't match")
|
||||
end
|
||||
|
||||
@layer = Carto::Layer.where(id: @layer_id).first
|
||||
raise LoadError.new("Layer not found: #{@layer_id}") unless @layer
|
||||
|
||||
raise UnprocesableEntityError.new("Layer #{@layer_id} doesn't belong to map #{@map_id}") unless @map.contains_layer?(@layer)
|
||||
end
|
||||
|
||||
def load_widget_id
|
||||
@widget_id = params[:id]
|
||||
if [@widget_id, params[:id]].compact.uniq.length >= 2
|
||||
raise UnprocesableEntityError.new("URL id (#{@widget_id}) and payload id (#{params[:id]}) don't match")
|
||||
end
|
||||
end
|
||||
|
||||
def source_id_from_params(parameters = params)
|
||||
parameters[:source] ? parameters[:source][:id] : nil
|
||||
end
|
||||
|
||||
def load_widget
|
||||
@widget = widget_with_validations(@widget_id, @layer_id)
|
||||
end
|
||||
|
||||
def widget_with_validations(widget_id, layer_id = nil)
|
||||
widget = Carto::Widget.find(widget_id)
|
||||
|
||||
raise Carto::LoadError.new("Widget not found: #{@widget_id}") unless widget
|
||||
|
||||
if layer_id && widget.layer_id != layer_id
|
||||
raise Carto::LoadError.new("Widget not found: #{@widget_id}")
|
||||
end
|
||||
|
||||
unless widget.belongs_to_map?(@map_id)
|
||||
raise Carto::LoadError.new("Widget not found: #{@widget_id} for that map (#{@map_id})")
|
||||
end
|
||||
|
||||
unless widget.writable_by_user?(current_user)
|
||||
raise Carto::UnauthorizedError.new("Not authorized for widget #{@widget_id}")
|
||||
end
|
||||
|
||||
widget
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user