refinery/refinerycms

View on GitHub
core/lib/refinery/crud.rb

Summary

Maintainability
C
1 day
Test Coverage
# Base methods for CRUD actions
# Simply override any methods in your action controller you want to be customised
# Don't forget to add:
#   resources :plural_model_name_here
# or for scoped:
#   scope(:as => 'module_module', :module => 'module_name') do
#      resources :plural_model_name_here
#    end
# to your routes.rb file.
# Full documentation about CRUD and resources go here:
# -> http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Resources.html#method-i-resources
# Example (add to your controller):
# crudify :foo, :title_attribute => 'name' for CRUD on Foo model
# crudify :'foo/bar', :title_attribute => 'name' for CRUD on Foo::Bar model
# Note: @singular_name will result in @foo for :foo and @bar for :'foo/bar'

module Refinery
  module Crud

    def self.default_options(model_name)
      class_name = "#{model_name.to_s.camelize.gsub('/', '::')}".gsub('::::', '::')
      this_class = class_name.constantize.base_class
      singular_name = ActiveModel::Naming.param_key(this_class)
      plural_name = singular_name.pluralize

      {
        :conditions => '',
        :include => [],
        :order => ('position ASC' if this_class.connected? && this_class.table_exists? && this_class.column_names.include?('position')),
        :paging => true,
        :per_page => false,
        :redirect_to_url => "refinery.#{Refinery.route_for_model(class_name.constantize, :plural => true)}",
        :searchable => true,
        :search_conditions => '',
        :sortable => true,
        :title_attribute => "title",
        :class_name => class_name,
        :singular_name => singular_name,
        :plural_name => plural_name
      }
    end

    def self.append_features(base)
      super
      base.extend(ClassMethods)
    end

    module ClassMethods

      def crudify(model_name, options = {})
        options = ::Refinery::Crud.default_options(model_name).merge(options)
        class_name = options[:class_name]
        singular_name = options[:singular_name]
        plural_name = options[:plural_name]

        module_eval <<-RUBY, __FILE__, __LINE__ + 1
          def self.crudify_options
            #{options.inspect}
          end

          prepend_before_action :find_#{singular_name},
                                :only => [:update, :destroy, :edit, :show]
          prepend_before_action :merge_position_into_params!, :only => :create

          def new
            @#{singular_name} = #{class_name}.new
          end

          def create
            @#{singular_name} = #{class_name}.new(#{singular_name}_params)
            if @#{singular_name}.save
              flash.notice = t(
                'refinery.crudify.created',
                :what => "'\#{@#{singular_name}.#{options[:title_attribute]}}'"
              )

              create_or_update_successful
            else
              create_or_update_unsuccessful 'new'
            end
          end

          def edit
            # object gets found by find_#{singular_name} function
          end

          def update
            if @#{singular_name}.update(#{singular_name}_params)
              flash.notice = t(
                'refinery.crudify.updated',
                :what => "'\#{@#{singular_name}.#{options[:title_attribute]}}'"
              )

              create_or_update_successful
            else
              create_or_update_unsuccessful 'edit'
            end
          end

          def destroy
            # object gets found by find_#{singular_name} function
            title = @#{singular_name}.#{options[:title_attribute]}
            if @#{singular_name}.destroy
              flash.notice = t('destroyed', :scope => 'refinery.crudify', :what => "'\#{title}'")
            end

            redirect_to redirect_url
          end

          # Finds one single result based on the id params.
          def find_#{singular_name}
            @#{singular_name} = find_#{singular_name}_scope.find(params[:id])
          end

          def find_#{singular_name}_scope
            _finder_scope = #{class_name}.includes(#{options[:include].map(&:to_sym).inspect})
            _finder_scope = _finder_scope.friendly if _finder_scope.respond_to?(:friendly)
            _finder_scope
          end

          # Find the collection of @#{plural_name} based on the conditions specified into crudify
          # It will be ordered based on the conditions specified into crudify
          # And eager loading is applied as specified into crudify.
          def find_all_#{plural_name}(conditions = #{options[:conditions].inspect})
            @#{plural_name} = find_#{singular_name}_scope
                                .where(conditions)
                                .order("#{options[:order]}")
          end

          def merge_position_into_params!
            # if the position field exists, set this object as last object, given the conditions of this class.
            if #{class_name}.column_names.include?("position") && params[:#{singular_name}][:position].nil?
              params[:#{singular_name}].merge!({
                position: ((#{class_name}.where(#{options[:conditions].inspect}).maximum(:position)||-1) + 1)
              })
            end
          end

          # Paginate a set of @#{plural_name} that may/may not already exist.
          def paginate_all_#{plural_name}
            # If we have already found a set then we don't need to again
            find_all_#{plural_name} if @#{plural_name}.nil?

            @#{plural_name} = @#{plural_name}.paginate(:page => params[:page], :per_page => paginate_per_page)
          end

          def paginate_per_page
            if #{options[:per_page].present?.inspect}
              #{options[:per_page].inspect}
            elsif #{class_name}.methods.map(&:to_sym).include?(:per_page)
              #{class_name}.per_page
            end
          end

          def redirect_url
            if params[:page].present?
              page = params[:page].to_i rescue 1
              page -= 1 while #{class_name}.paginate(:page => page).empty? && page > 1
              #{options[:redirect_to_url]}(:page => page)
            else
              #{options[:redirect_to_url]}
            end
          end

          # If the controller is being accessed via an ajax request
          # then render only the collection of items.
          def render_partial_response?
            if request.xhr?
              render plain: render_to_string(partial: '#{plural_name}', layout: false).html_safe,
                     layout: 'refinery/flash' and return false
            end
          end

          def create_or_update_successful
            if from_dialog?
              self.index
              @dialog_successful = true
              render :index
            else
              if /true|on|1/ === params[:continue_editing]
                if request.xhr?
                  render :partial => '/refinery/message'
                else
                  redirect_back(fallback_location: { action: 'edit' })
                end
              else
                redirect_back_or_default redirect_url
              end
            end
          end

          def create_or_update_unsuccessful(action)
            if request.xhr?
              render :partial => '/refinery/admin/error_messages', :locals => {
                       :object => @#{singular_name},
                       :include_object_name => true
                     }
            else
              render :action => action
            end
          end

          # Returns a weighted set of results based on the query specified by the user.
          def search_all_#{plural_name}
            # First find normal results.
            find_all_#{plural_name}(#{options[:search_conditions].inspect})

            # Now get weighted results by running the query against the results already found.
            @#{plural_name} = @#{plural_name}.with_query(params[:search])
          end

          def #{singular_name}_params
            raise "Please override #{singular_name}_params with your desired parameter security."
          end

          # Ensure all methods are protected so that they should only be called
          # from within the current controller.
          protected :find_#{singular_name},
                    :find_#{singular_name}_scope,
                    :find_all_#{plural_name},
                    :paginate_all_#{plural_name},
                    :paginate_per_page,
                    :render_partial_response?,
                    :search_all_#{plural_name},
                    :#{singular_name}_params,
                    :redirect_url,
                    :create_or_update_successful,
                    :create_or_update_unsuccessful,
                    :merge_position_into_params!
        RUBY

        # Methods that are only included when this controller is searchable.
        if options[:searchable]
          if options[:paging]
            module_eval <<-RUBY, __FILE__, __LINE__ + 1
              def index
                search_all_#{plural_name} if searching?
                paginate_all_#{plural_name}

                render_partial_response?
              end
            RUBY
          else
            module_eval <<-RUBY, __FILE__, __LINE__ + 1
              def index
                if searching?
                  search_all_#{plural_name}
                else
                  find_all_#{plural_name}
                end

                render_partial_response?
              end
            RUBY
          end

        else
          if options[:paging]
            module_eval <<-RUBY, __FILE__, __LINE__ + 1
              def index
                paginate_all_#{plural_name}

                render_partial_response?
              end
            RUBY
          else
            module_eval <<-RUBY, __FILE__, __LINE__ + 1
              def index
                find_all_#{plural_name}
                render_partial_response?
              end
            RUBY
          end

        end

        if options[:sortable]
          module_eval <<-RUBY, __FILE__, __LINE__ + 1
            def reorder
              find_all_#{plural_name}
            end

            # Based upon https://github.com/matenia/jQuery-Awesome-Nested-Set-Drag-and-Drop
            def update_positions
              previous = nil
              params.to_unsafe_h[:ul].each do |_, list|
                list.each do |index, hash|
                  moved_item_id = hash['id'][/\\d+\\z/]
                  @current_#{singular_name} = #{class_name}.find_by_id(moved_item_id)

                  if @current_#{singular_name}.respond_to?(:move_to_root)
                    if previous.present?
                      @current_#{singular_name}.move_to_right_of(#{class_name}.find_by_id(previous))
                    elsif !@current_#{singular_name}.root?
                      @current_#{singular_name}.move_to_root
                    end
                  else
                    @current_#{singular_name}.update_columns position: index
                  end

                  if hash['children'].present?
                    update_child_positions(hash, @current_#{singular_name})
                  end

                  previous = moved_item_id
                end
              end

              #{class_name}.rebuild! if #{class_name}.respond_to?(:rebuild!)

              after_update_positions
            end

            def update_child_positions(_node, #{singular_name})
              list = _node['children']['0']
              child_positions_changed = false
              list.sort_by { |k, v| k.to_i}.map { |item| item[1] }.each_with_index do |child, index|
                child_id = child['id'].split(/#{singular_name}\_?/).reject(&:empty?).first
                child_#{singular_name} = #{class_name}.where(:id => child_id).first
                child_positions_changed ||= #{singular_name}.children[index] != child_#{singular_name}
                if child_positions_changed
                  child_#{singular_name}.move_to_child_of(#{singular_name})
                end

                if child['children'].present?
                  update_child_positions(child, child_#{singular_name})
                end
              end
            end

            def after_update_positions
              head :ok
            end

            protected :after_update_positions
          RUBY
        end

        module_eval <<-RUBY, __FILE__, __LINE__ + 1
          class << self
            def pageable?
              #{options[:paging]}
            end
            alias_method :paging?, :pageable?

            def sortable?
              #{options[:sortable]}
            end

            def searchable?
              #{options[:searchable]}
            end
          end
        RUBY
      end

    end

  end
end