require_dependency 'carto/uuidhelper' require_relative '../builder/builder_users_module' module Carto module Api class AnalysesController < ::Api::ApplicationController 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 StandardError => 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 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