activescaffold/active_scaffold

View on GitHub
lib/active_scaffold/actions/core.rb

Summary

Maintainability
D
2 days
Test Coverage
F
42%
module ActiveScaffold::Actions
  module Core
    def self.included(base)
      base.class_eval do
        before_action :set_vary_accept_header
        before_action :check_input_device
        before_action :register_constraints_with_action_columns, :unless => :nested?
        after_action :clear_flashes
        after_action :dl_cookie
        around_action :clear_storage
        rescue_from ActiveScaffold::RecordNotAllowed, ActiveScaffold::ActionNotAllowed, :with => :deny_access
      end
      base.helper_method :active_scaffold_config
      base.helper_method :successful?
      base.helper_method :nested?
      base.helper_method :grouped_search?
      base.helper_method :embedded?
      base.helper_method :loading_embedded?
      base.helper_method :calculate_query
      base.helper_method :new_model
      base.helper_method :touch_device?
      base.helper_method :hover_via_click?
    end

    def render_field
      if request.get? || request.head?
        render_field_for_inplace_editing
        respond_to do |format|
          format.js { render :action => 'render_field_inplace', :layout => false }
        end
      else
        render_field_for_update_columns
        respond_to { |format| format.js }
      end
    end

    protected

    def loading_embedded?
      @loading_embedded ||= active_scaffold_embedded_params.delete(:loading)
    end

    def embedded?
      params[:eid]
    end

    def nested?
      false
    end

    def grouped_search?
      false
    end

    def render_field_for_inplace_editing
      @column = active_scaffold_config.columns[params[:update_column]]
      @record = find_if_allowed(params[:id], :crud_type => :update, :column => params[:update_column])
    end

    def render_field_for_update_columns
      return if (@column = active_scaffold_config.columns[params.delete(:column)]).nil?
      @source_id = params.delete(:source_id)
      @columns = @column.update_columns || []
      @scope = params.delete(:scope)
      if @scope
        @form_action = :subform
      elsif active_scaffold_config.actions.include? params[:form_action]&.to_sym
        @form_action = params.delete(:form_action).to_sym
      end
      @form_action ||= params[:id] ? :update : :create
      @main_columns = active_scaffold_config.send(@form_action).columns
      @columns << @column.name if @column.options[:refresh_link] && @columns.exclude?(@column.name)

      @record =
        if @column.send_form_on_update_column
          updated_record_with_form(@main_columns, params[:record] || params[:search], @scope)
        else
          updated_record_with_column(@column, params.delete(:value), @scope)
        end
      # if @scope has more than 2 ] then it's subform inside subform, and assign parent would fail (found associotion may be through association)
      setup_parent(@record) if main_form_controller && @scope && @scope.scan(']').size == 2
      after_render_field(@record, @column)
    end

    def updated_record_with_form(columns, attributes, scope)
      if attributes && scope
        attributes = scope.delete('[').split(']').inject(attributes) { |h, idx| h[idx] }
        id = attributes[:id]
      else
        id = params[:id]
      end

      # check permissions and support overriding to_param
      saved_record = find_if_allowed(id, :read) if id
      # call update_record_from_params with new_model
      # in other case some associations can be saved
      record = new_model
      copy_attributes(saved_record, record) if saved_record
      apply_constraints_to_record(record) unless scope
      create_association_with_parent record, true if nested?
      update_record_from_params(record, columns, attributes || {}, true)
    end

    def updated_record_with_column(column, value, scope)
      record = params[:id] ? copy_attributes(find_if_allowed(params[:id], :read)) : new_model
      apply_constraints_to_record(record) unless scope || params[:id]
      create_association_with_parent record, true if nested?
      if @form_action == :field_search && value.is_a?(Array) && column.association&.singular?
        # don't assign value if it's an array and column is singular association,
        # e.g. value came from multi-select on search form
        # use instance variable so it's available in the view and helpers
        @value = value
      else
        update_column_from_params(record, column, value, true)
      end
      record.id = params[:id]
      record
    end

    def subform_child_association
      params[:child_association].presence || @scope&.split(']')&.first&.sub(/^\[/, '').presence
    end

    def parent_controller_name
      "#{params[:parent_controller].camelize}Controller"
    end

    def setup_parent(record)
      cfg = main_form_controller.active_scaffold_config
      association = cfg.columns[subform_child_association]&.association&.reverse_association
      return if association.nil?

      parent_model = cfg.model
      parent = parent_model.new
      copy_attributes(find_if_allowed(params[:parent_id], :read, parent_model), parent) if params[:parent_id]
      parent.id = params[:parent_id]
      parent = update_record_from_params(parent, cfg.send(params[:parent_id] ? :update : :create).columns, params[:record], true) if @column.send_form_on_update_column
      apply_constraints_to_record(parent) unless params[:parent_id]
      if association.collection?
        record.send(association.name) << parent
      else
        record.send("#{association.name}=", parent)
      end

      if params[:nested] # form in nested scaffold, set nested parent_record to parent
        nested = ActiveScaffold::DataStructures::NestedInfo.get(parent.class, params[:nested])
        if nested&.child_association && !nested.child_association.polymorphic?
          apply_constraints_to_record(parent, constraints: {nested.child_association.name => nested.parent_id})
        end
      end
      parent
    end

    def copy_attributes(orig, dst = nil)
      dst ||= orig.class.new
      orig.attributes.each { |attr, value| dst.send :write_attribute, attr, value }
      dst
    end

    def parent_sti_controller
      return unless params[:parent_sti]
      unless defined? @parent_sti_controller
        controller = look_for_parent_sti_controller
        @parent_sti_controller = controller.controller_path == params[:parent_sti] ? controller : false
      end
      @parent_sti_controller
    end

    # override this method if you want to do something after render_field
    def after_render_field(record, column); end

    def authorized_for?(options = {})
      active_scaffold_config.model.authorized_for?(options)
    end

    def clear_flashes
      flash.clear if request.xhr?
    end

    def dl_cookie
      cookies[params[:_dl_cookie]] = {value: Time.now.to_i, expires: 1.day.since} if params[:_dl_cookie]
    end

    def each_marked_record(&block)
      active_scaffold_config.model.as_marked.each(&block)
    end

    def marked_records
      active_scaffold_session_storage['marked_records'] ||= {}
    end

    def default_formats
      %i[html js json xml]
    end

    # Returns true if the client accepts one of the MIME types passed to it
    # ex: accepts? :html, :xml
    def accepts?(*types)
      request.accepts.compact.each do |priority|
        # Because IE always sends */* in the accepts header and we assume
        # that if you really wanted XML or something else you would say so
        # explicitly, we will assume */* to only ask for :html
        return types.include?(:html) if priority == Mime::ALL

        return true if types.include?(priority.to_sym)
      end
      false
    end

    def response_status
      if successful?
        action_name == 'create' ? 201 : 200
      else
        422
      end
    end

    # API response object that will be converted to XML/JSON using to_xxx
    def response_object
      @response_object ||= successful? ? (@record || @records) : @record.errors
    end

    def response_to_api(format, columns_names, options = {})
      render(
        options.reverse_merge(
          format => response_object,
          :only => columns_names + [active_scaffold_config.model.primary_key],
          :include => association_columns(columns_names),
          :methods => virtual_columns(columns_names),
          :status => response_status
        )
      )
    end

    # Success is the existence of one or more model objects. Most actions
    # circumvent this method by setting @success directly.
    def successful?
      if @successful.nil?
        true
      else
        @successful
      end
    end

    def successful=(val)
      @successful = val ? true : false
    end

    # Redirect to the main page (override if the ActiveScaffold is used as a component on another controllers page) for Javascript degradation
    def return_to_main
      options = main_path_to_return
      # use url_for in case main_path_to_return returns Hash with status param,
      # which would be interpreted as status option to redirect_to instead of url param
      redirect_to options.is_a?(Hash) ? url_for(options) : options
    end

    # Overide this method on your controller to provide model with named scopes
    def beginning_of_chain
      active_scaffold_config.model
    end

    # Builds search conditions by search params for column names. This allows urls like "contacts/list?company_id=5".
    def conditions_from_params
      @conditions_from_params ||= begin
        conditions = [{}]
        params.except(:controller, :action, :page, :sort, :sort_direction, :format, :id).each do |key, value|
          distinct = true if key.match?(/!$/)
          column = active_scaffold_config._columns_hash[key.to_s[0..(distinct ? -2 : -1)]]
          next unless column
          key = column.name.to_sym
          not_string = %i[string text].exclude?(column.type)
          next if active_scaffold_constraints[key]
          next if nested? && nested.param_name == key

          range = %i[date datetime integer decimal float bigint].include?(column.type) && value.is_a?(String) && value.scan('..').size == 1
          value = value.split('..') if range
          value =
            if value.is_a?(Array)
              value.map { |v| v == '' && not_string ? nil : ActiveScaffold::Core.column_type_cast(v, column) }
            elsif value == '' && (not_string || column.null)
              ActiveScaffold::Core.column_type_cast(column.default, column)
            else
              ActiveScaffold::Core.column_type_cast(value, column)
            end
          value = Range.new(*value) if range
          if distinct
            conditions << active_scaffold_config.model.arel_table[key].not_eq(value)
          else
            conditions[0][key] = value
          end
        end
        conditions
      end
    end

    def new_model
      relation = beginning_of_chain
      if nested? && nested.plural_association? && nested.match_model?(active_scaffold_config.model)
        build_options = sti_nested_build_options(relation.klass)
      end
      relation.respond_to?(:build) ? relation.build(build_options || {}) : relation.new
    end

    def sti_nested_build_options(klass)
      config = active_scaffold_config_for(klass)
      return unless config
      column = klass.inheritance_column
      return unless column && config._columns_hash[column]

      model_name = params.delete(column) # in new action inheritance_column must be in params
      model_name ||= params[:record]&.delete(column) # in create action must be inside record key
      model_name = model_name.camelize if model_name
      model_name ||= active_scaffold_config.model.name
      {column.to_sym => model_name} if model_name
    end

    def get_row(crud_type_or_security_options = :read)
      klass = beginning_of_chain
      klass = klass.preload(active_scaffold_preload) unless active_scaffold_config.mongoid?
      @record = find_if_allowed(params[:id], crud_type_or_security_options, klass)
    end

    def active_scaffold_embedded_params
      params[:embedded] || {}
    end

    def clear_storage
      yield if block_given?
    ensure
      session_index = active_scaffold_session_storage_key
      session.delete(session_index) if session[session_index].blank?
    end

    def set_vary_accept_header
      response.headers['Vary'] = 'Accept'
    end

    def check_input_device
      return unless session[:input_device_type].nil?
      return if request.env['HTTP_USER_AGENT'].nil?
      if request.env['HTTP_USER_AGENT'].match?(/(iPhone|iPod|iPad)/i)
        session[:input_device_type] = 'TOUCH'
        session[:hover_supported] = false
      else
        session[:input_device_type] = 'MOUSE'
        session[:hover_supported] = true
      end
    end

    def touch_device?
      session[:input_device_type] == 'TOUCH'
    end

    def hover_via_click?
      session[:hover_supported] == false
    end

    def params_hash?(value)
      value.is_a?(Hash) || controller_params?(value)
    end

    def controller_params?(value)
      value.is_a?(::ActionController::Parameters)
    end

    def params_hash(value)
      if controller_params?(value)
        value.to_unsafe_h.with_indifferent_access
      else
        value
      end
    end

    # call this method in your action_link action to simplify processing of actions
    # eg for member action_link :fire
    # process_action_link_action do |record|
    #   record.update_attributes(:fired => true)
    #   self.successful = true
    #   flash[:info] = 'Player fired'
    # end
    def process_action_link_action(render_action = :action_update, crud_type_or_security_options = nil)
      if request.get? || request.head?
        # someone has disabled javascript, we have to show confirmation form first
        @record = find_if_allowed(params[:id], :read) if params[:id]
        respond_to_action(:action_confirmation)
      else
        @action_link = active_scaffold_config.action_links[action_name]
        if params[:id]
          crud_type_or_security_options ||= {:crud_type => request.delete? ? :delete : :update, :action => action_name}
          get_row(crud_type_or_security_options)
          if @record.nil?
            self.successful = false
            flash[:error] = as_(:no_authorization_for_action, :action => action_name)
          else
            yield @record
          end
        else
          if @action_link && respond_to?(@action_link.security_method, true) && !send(@action_link.security_method)
            raise ActiveScaffold::ActionNotAllowed
          end
          yield
        end
        respond_to_action(render_action)
      end
    end

    def action_confirmation_respond_to_html(confirm_action = action_name.to_sym)
      link = active_scaffold_config.action_links[confirm_action]
      render :action => 'action_confirmation', :locals => {:record => @record, :link => link}
    end

    def action_update_respond_on_iframe
      responds_to_parent { action_update_respond_to_js }
    end

    def action_update_respond_to_html
      redirect_to :action => 'index'
    end

    def action_update_respond_to_js
      render :action => 'on_action_update', :formats => [:js], :layout => false
    end

    def action_update_respond_to_xml
      response_to_api(:xml, list_columns_names)
    end

    def action_update_respond_to_json
      response_to_api(:json, list_columns_names)
    end

    def objects_for_etag
      @last_modified ||= @record.updated_at
      [@record, ('xhr' if request.xhr?)]
    end

    def view_stale?
      objects = objects_for_etag
      if objects.is_a?(Array)
        args = {:etag => objects.to_a}
        args[:last_modified] = @last_modified if @last_modified
      elsif objects.is_a?(Hash)
        args = {:last_modified => @last_modified}.merge(objects)
      else
        args = objects
      end
      stale?(args)
    end

    def conditional_get_support?
      request.get? && active_scaffold_config.conditional_get_support
    end

    def virtual_columns(columns)
      columns.reject do |col|
        active_scaffold_config._columns_hash[col.to_s] || active_scaffold_config.columns[col]&.association
      end
    end

    def association_columns(columns)
      columns.select { |col| active_scaffold_config.columns[col]&.association }
    end

    private

    def respond_to_action(action)
      return unless !conditional_get_support? || view_stale?
      respond_to do |type|
        action_formats.each do |format|
          type.send(format) do
            method_name = respond_method_for(action, format)
            send(method_name) if method_name
          end
        end
      end
    end

    def respond_method_for(action, format)
      if format == :html && params[:iframe] == 'true'
        method_name = "#{action}_respond_on_iframe"
        return method_name if respond_to?(method_name, true)
      end
      method_name = "#{action}_respond_to_#{format}"
      method_name if respond_to?(method_name, true)
    end

    def action_formats
      @action_formats ||=
        if respond_to? "#{action_name}_formats", true
          send("#{action_name}_formats")
        else
          (default_formats + active_scaffold_config.formats).uniq
        end
    end

    def look_for_parent_sti_controller
      klass = self.class.active_scaffold_config.model
      loop do
        klass = klass.superclass
        controller = self.class.active_scaffold_controller_for(klass)
        cfg = controller.active_scaffold_config if controller.uses_active_scaffold?
        next unless cfg&.add_sti_create_links?
        return controller if cfg.sti_children.map(&:to_s).include? self.class.active_scaffold_config.model.name.underscore
      end
    rescue ActiveScaffold::ControllerNotFound => ex
      logger.warn "#{ex.message} looking for parent_sti of #{self.class.active_scaffold_config.model.name}"
      nil
    end
  end
end