表示順を変更できるようにする

前回は管理者が質問の順序を変更できるようにルーティングを追加しました。
今回はモデルとコントローラーを作成して対象を上下に移動できるようにします。

コントローラー名は先日読んだWritebookのソースコードの影響を受けています。Writebookは複数の対象をドラッグ・アンド・ドロップできるようになっていて、とても勉強になりました。

# config/routes.rb
  resources :questions, only: %i[ index show ] do
    resources :moves, only: :create, module: :questions
  end

モデル Question に表示順に使う sort_order を追加します。
浮動小数を使う方法もありますが今回は整数を使います。

bin/rails g migration add_sort_order_to_questions sort_order:integer:uniq

sort_order は null値を受け入れないように null: false を追加します。

# db/migrate/..._add_sort_order_to_questions.rb
add_column :questions, :sort_order, :integer, null: false
bin/rails db:migrate

モデルにソート順、バリデーション、上下移動のメソッドを追加します。

# app/models/question.rb
class Question < ApplicationRecord
  RESERVED_SORT_ORDER = -1

  scope :positioned, -> { order(:sort_order) }

  validates :sort_order, presence: true, uniqueness: true

  def move_up
    above = Question.find_by(sort_order: sort_order - 1)
    Question.swap_position(above, self)
  end

  def move_down
    below = Question.find_by(sort_order: sort_order + 1)
    Question.swap_position(self, below)
  end

  def self.swap_position(upper, lower)
    return if upper.nil? || lower.nil?

    ActiveRecord::Base.transaction do
      saved_sort_order = upper.sort_order
      upper.update!(sort_order: RESERVED_SORT_ORDER)
      lower.update!(sort_order: saved_sort_order)
      upper.update!(sort_order: saved_sort_order + 1)
    end
  end
end

seedデータにsort_orderを追加します。
判別しやすいようにラジオボタンの質問にA、チェックボックスの質問にBを付けています。

User.find_or_create_by(name: "ユーザー1")
User.find_or_create_by(name: "ユーザー2")
qr1 = QuestionRadio.find_or_create_by(content: "A1 好きな映画のジャンルは?", sort_order: 1)
["アクション", "コメディー", "ドラマ", "ホラー"].each do |content|
  Radio.find_or_create_by(content: content, question: qr1)
end
qr2 = QuestionRadio.find_or_create_by(content: "A2 好きな果物は?", sort_order: 2)
["いちご", "もも", "みかん", "りんご", "ぶどう"].each do |content|
  Radio.find_or_create_by(content: content, question: qr2)
end
qc1 = QuestionCheck.find_or_create_by(content: "B1 好きな映画のジャンルは?", sort_order: 3)
["アクション", "コメディー", "ドラマ", "ホラー"].each do |content|
  Check.find_or_create_by(content: content, question: qc1)
end
qc2 = QuestionCheck.find_or_create_by(content: "B2 好きな果物は?", sort_order: 4)
["いちご", "もも", "みかん", "りんご", "ぶどう"].each do |content|
  Check.find_or_create_by(content: content, question: qc2)
end

テーブルとseedデータを作り直します。

bin/rails db:reset

コントローラーを生成します。

bin/rails g controller "questions/moves" --no-helper

引数directionがupなら上へ、downなら下に移動します。

# app/controllers/questions/moves_controller.rb
class Questions::MovesController < ApplicationController
  def create
    question = Question.find(params[:question_id])
    case params[:direction]
    when "up"
      if question.move_up
        redirect_to questions_url
      end
    when "down"
      if question.move_down
        redirect_to questions_url
      end
    end
  end
end

ボタンはインラインで並ぶようにします。

# app/views/questions/index.html.erb
<% @questions.positioned.each do |question| %>
  <div>
    <%= link_to question.content,
        question.becomes(Question)
    %>
    <%= button_to "Up",
        question_moves_path(question),
        params: { direction: "up" },
        form: { style: "display: inline" }
    %>
    <%= button_to "Down",
        question_moves_path(question),
        params: { direction: "down" },
        form: { style: "display: inline" }
    %>
  </div>
<% end %>

以上になります。