【Rails5】acts_as_listでデータの並び替え機能を実装
一覧ページ(index)でよく使いたくなるデータの並び替え機能について
Gemをインストール
acts_as_listというgemを使います。
Gemfileに記載してbundle installしましょう。
【Gemfile】
gem 'acts_as_list'
並べ替えしたいデータのモデルを編集
Userモデルで実装するとしましょう。
userモデルに「acth_as_list」の1行を追加。
【models/user.rb】
class User < ApplicationRecord acts_as_list end
これでデータの作成時にpositionカラムにデータが入るようになります。
※注意!Userテーブルのカラムにpositionがないと動きません、ない場合はrails g migrationで付け加えてください
moveアクションの追加
アクション名はなんでもいいんですが(例えばchange_positionとか)、今回はmoveで。
users_controllerにmoveアクションを定義します。
indexの@userを作る箇所ではpositionでオーダーかける必要があります。
【controllers/users_controller.rb】
class UsersController < ApplicationController def index @user = User.all.order(:position) end def move case params[:move] when 'up' @user.move_higher @target = @user.lower_item when 'down' @user.move_lower @target = @user.higher_item else return head :ok end end end
moveアクションを見ると、params[:move]が飛んでくることがわかりますので、この後viewで飛ばすように実装します。
params[:move]の値がupならmove_higherで上に移動、downならmove_lowerで下に移動をしていますね。
アクション定義したのでroutesに追記
新規アクションを設定したのでroutesを変更しましょう。
【routes.rb】
Rails.application.routes.draw do resources :users do member do get :move end end end
viewの編集
設定が終わったのでviewを作っていきます。
index.html.erbでユーザ一覧を確認できるようにテーブル組みで作成。
左端にはpositionの入れ替えをするlink_toを用意します。
【users/index.html.erb】
<table class="list_table mb_l"> <tr> <th colspan="2"></th> <th><%=f User, :name %></th> <th><%=f User, :email %></th> <th><%=f User, :created_at %></th> <th></th> </tr> <% @users.each do |user| %> <tr> <td><%= link_to '▲', move__user_path(id: user, move: 'up'), remote: true unless user.first? %></td> <td><%= link_to '▼', move_user_path(id: user, move: 'down'), remote: true unless user.last? %></td> <td><%= user.name %></td> <td><%= user.email %></td> <td><%=l user.created_at, format: :long %></td> <td><%= link_to t('link.delete'), user_path(user), method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> </table>
@usersをeachで回したtrを入れ替えたいので部分テンプレートにします。
【users/index.html.erb】
<table class="list_table mb_l"> <tr> <th colspan="2"></th> <th><%=f User, :name %></th> <th><%=f User, :email %></th> <th><%=f User, :created_at %></th> <th></th> </tr> <% @users.each do |user| %> <%= render 'user_list', user: user %> <% end %> </table>
部分テンプレートではpositionの変更をした後に表示を切り替えるjsのトリガーを追記します。
また、jsで選択できるようにtrにidを付けておきます。
【users/_users_list.html.erb】
<tr id="user_<%= user.position %>" > <td><%= link_to '▲', move__user_path(id: user, move: 'up'), remote: true unless user.first? %></td> <td><%= link_to '▼', move_user_path(id: user, move: 'down'), remote: true unless user.last? %></td> <td><%= user.name %></td> <td><%= user.email %></td> <td><%=l user.created_at, format: :long %></td> <td><%= link_to t('link.delete'), user_path(user), method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr>
表示の切替を行うjsを作成
viewの▼のリンクにremote: trueを付けました、クリックされたら呼ばれるjsを実装していきます。
まずはusers/move.coffeeファイルを作成。
その後一覧ページの表示切替の処理を記載します。
【users/move.coffee】
$("#user_<%= @user.position %>").replaceWith('<%=j render "user_list", user: @user %>') $("#user_<%= @target.position %>").replaceWith('<%=j render "user_list", user: @target %>')
これで実装完了!
まとめ
慣れれば簡単に実装できますが、変更するファイルが多いのとcoffeeファイルを使うので初心者の方は難しく感じると思います(私もそうでした)。
ruby、railsを覚えてくると何をやっているかわかるようになるはず!
業務では設定の管理ページなどでよく使っています。
筆者オススメ技術書
【Rails】1つのフォームタグで紐付いたテーブルの項目も登録したい場合
例えば「企業(company)」テーブルと「事業所(office)」テーブルがあるとします。
企業と事務所の関係は、企業が複数の事業所を持っているということで、1対多の関係です。
モデルの紐付けと設定
親のcompanyに「accepts_nested_attributes_for :offices」の1文を記載する。
【models/company.rb】
class Company < ApplicationRecord has_many :offices, inverse_of: :company accepts_nested_attributes_for :offices end
【models/office.rb】
class Office < ApplicationRecord belongs_to :company, inverse_of: :offices end
inverse_ofはcompanyもofficeも新規で作成する場合、もとにするcompanyのidが見つからないというエラーを起こすので、それを防ぐために記載する
@companyの定義と関連テーブルのbuild
@companyを定義し、紐づく@officesをbuildします
【controllers/company_controller.rb】
class CompaniesController < ApplicationController def new @company = Company.new @office = @company.offices.build end end
追加でparamsの受け取りも定義してやります。
class CompaniesController < ApplicationController def new @company = Company.new @office = @company.offices.build end private def company_params params.require(:company).permit( :name, :url, offices_attributes: [:id, :company_id, :name, :postal, :prefecture, :address, :building, :tel, :fax, :remark] ) end end
最後にviewのフォームを設定
@companyの項目はいつも通りに、buildしたofficeの項目について「fields_for」で入力項目を出します。
※controllerでbuildしないと入力項目が表示されません
【views/companies/_form.html.erb】
<%= form_with(model: @company) do |form| %> <table> <tr> <th rowspan="3"><%= form.label f(Company, :name) %></th> <td class="sub"><%=f Company, :name %></td> <td><%= form.text_field :name %></td> </tr> <tr> <th><%= form.label f(Company, :url) %></th> <td colspan="2"><%= form.text_field :url %></td> </tr> <%= form.fields_for :offices do |office| %> <tr> <th><%= office.label :postal %></th> <td colspan="2"> <%= office.text_field :postal %> </td> </tr> <tr> <th><%= office.label :prefecture %></th> <td colspan="2"> <%= office.select :prefecture, PrefectureList %> </td> </tr> <tr> <th><%= office.label :address %></th> <td colspan="2"> <%= office.text_field :address %> </td> </tr> <tr> <th><%= office.label :building %></th> <td colspan="2"> <%= office.text_field :building %> </td> </tr> <tr> <th><%= office.label :tel %></th> <td colspan="2"> <%= office.text_field :tel %> </td> </tr> <tr> <th><%= office.label :fax %></th> <td colspan="2"> <%= office.text_field :fax %> </td> </tr> <tr> <th><%= office.label :remark %></th> <td colspan="2"> <%= office.text_area :remark %> </td> </tr> <% end %> </table> <%= form.submit %> <% end %>
こんな感じでフォームを分けなくても一気に登録することができます。
筆者オススメ技術書
たのしいRuby5版TimeクラスとDateクラス練習問題(4)
プログラミング勉強会でたのしいRubyに取り組んでいます。
第20章 TimeクラスとDateクラスの練習問題(4)がなかなか難しく、回答を調べてもあんまりヒットしなかったので私の回答をメモ程度に掲載します。
問題文
Dateクラスを使って今月の1日と月末の日付と曜日を求め、次のような形式でカレンダーを表示させてください。
やってみた
自力で解くのは無理でした。
ひとまず解き方を理解するのに公式の回答を見ました…なんと「万年カレンダー」の原理を使った回答でした。
…万年カレンダーってなに?って感じだったので諦めて他の回答をネット上で探すことに。
しかしあまり数はないようですね…結局「プロを目指す人のためのRuby入門」の著者、伊藤淳一さんが回答をブログに載せていたので参考にすることに。
私的回答
会社の先輩にも手伝ってもらいながら出した回答が以下です!
require 'date' def calender(year, month) first_date = Date.new(year, month, 1) last_date = first_date.next_month.prev_day puts first_date.strftime("%B %Y").center(21) puts "Su Mo Tu We Th Fr Sa" week = [] (first_date..last_date).each do |day| week = [] if day.wday == 0 week[day.wday] = day.strftime("%e") puts week.map{|date| date ? date : " "}.join(" ") if day.wday == 6 or day == last_date end end calender(2018, 10)
- first_dateで1日を取得
- last_dateで月末の日を取得(次の月の1日前を取得)
- 1行目に表示する年と月を中央寄せで表示(2行目の文字列の数が21文字なのでcenterの引数は21)
- 2行目に表示する曜日は文字列でそのまま表示
- weekという空配列を作り、1日から月末日までをeachでまわした結果をweekに入れていく
- day.wday(土曜日)が月の最後の日のとき、weekを表示
- day.wdayが0(日曜日にきたら)weekを初期化(これで改行を入れている)
これでうまいこと表示できました! これ以外の回答ももちろんあるはずです! Rubyって便利だなぁ。
筆者オススメ技術書
【jQuery】ラジオボタンのfalseにチェック入っていたら日付選択をさせないようにしたい
業務で特定の操作の締め切り期限機能を作る機会がありました。
締め切り設定は使う・使わないを選択でき、使う場合は締切日をselectで選択、使わない場合はselectをdisabledにするという処理です。
実装
railsアプリなのでjQueryは既に読み込んでいます。
まずはjQueryを使えるようにしましょう。
【html.erb】
<table class='formTable'> <tr> <th width="160"><%=f Deadline, :use %></th> <td> <%= radio_button :deadline, :use, true %> <%= label_tag 'deadline_use_true', t('.use_true') %> <%= radio_button :deadline, :use, false %> <%= label_tag 'deadline_use_false', t('.use_false') %> </td> </tr> <tr> <th><%=f Deadline, :deadline_date %></th> <td> <%= date_select :deadline, :deadline_date, use_month_numbers: true, date_separator: ' / ', start_year: Time.zone.now.year, end_year: Time.zone.now.year + 3 %> <%= error_messages_on :deadline, :deadline_date %> </td> </tr> </table>
【js】
<script type="text/javascript"> $(function() { // 初期値をfalseに $('#deadline_use_false').prop('checked', true); // ページ読み込み時にfalseだったら日付を選択させない if ($('#deadline_use_false').prop('checked')) { $('#deadline_deadline_date_1i, #deadline_deadline_date_2i, #deadline_deadline_date_3i').prop('disabled', true); } // 使わないにチェックを入れたら日付選択させない $('#deadline_use_false').change(function() { $('#deadline_deadline_date_1i, #deadline_deadline_date_2i, #deadline_deadline_date_3i').prop('disabled', true); }); // 使うにチェックを入れたら日付選択可能 $('#deadline_use_true').change(function() { $('#deadline_deadline_date_1i, #deadline_deadline_date_2i, #deadline_deadline_date_3i').prop('disabled', false); }); }); </script>
prop()とは、属性プロパティに値を設定、または設定されている値を取得します。
参考:.prop() | jQuery 1.9 日本語リファレンス | js STUDIO
動作はコメントの通りで、初期値にfalseを入れています(普段は使わない設定に)。
マウスで使う・使わないを選択したらselectをdisabledする・しないを切り替えられるように処理を入れています。
ただ、ページを読み込んだときはラジオボタンの値がfalseなのに日付のselectが使える状態になっているため、if文でselectを使えないようにする処理を噛ませています。
railsアプリを使っているとこういったdisabledにする処理はちょいちょい見かけるのでメモしておきました。
筆者オススメ技術書
【CSS】ボックスのサイズをドラッグで自由に変更する
divなどのタグで囲まれたボックスのサイズを手動で変更したときは、「resize:both」を使います。
Can I useによると、主要ブラウザは軒並み対応してますね。
ただ、スマートフォンのブラウザはまだ怪しい感じです。
基本的な使い方
ボックスの右下に斜めの線が表示されていると思うので、ドラッグすると好きなサイズに調整できます。
resizeと一緒にoverflow: hidden;
を使わないと斜め線が出てこないので注意が必要です。
See the Pen BqPLRe by ooe (@oe526) on CodePen.
上の例ではresize: both;
としていますが、これは幅と高さ両方を自由に変更可能にするという指定です。
他の指定は以下に記述しておきます。
- none
リサイズ機能をなくす - both
幅、高さともにリサイズできる - horizontal
幅のみリサイズできる - vertical
高さのみリサイズできる - inherit
親要素の値を継承
応用編
業務中、videoタグで埋めた動画のサイズを手動で変更したという依頼がきたことがありました。
videoタグに直接resize: both;を指定してみましたがうまくいかなかったのでその時の解決法を載せます。
【html】
<div class="box"> <video src="動画ファイルのパス"></video> </div>
【css】
div { width: 200px; height: 200px; border: 1px solid #999; resize: both; overflow: hidden; } video { width: 100%; height: 100%; }
videoをdivでくくり、ボックスのサイズをdivで決めてしまう。
videoのサイズはwidth: 100%; height: 100%;でdivに依存させる。
divの方にresizeを指定すればvideoのサイズは外側のdivに引っ張られ、擬似的にリサイズが可能という寸法です。
筆者オススメ技術書
【ログイン機能】rails5でsorceryを実装
プロジェクトの作成
データベースはpostgresqlを指定
バンドルインストールをスキップ
rails new プロジェクト名 -d postgresql -B
Gemfileにsorceryを記載 【Gemfile】
source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.5.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 5.2.0' # Use postgresql as the database for Active Record gem 'pg', '>= 0.18', '< 2.0' # Use Puma as the app server gem 'puma', '~> 3.11' # Use SCSS for stylesheets gem 'sass-rails', '~> 5.0' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' # See https://github.com/rails/execjs#readme for more supported runtimes # gem 'mini_racer', platforms: :ruby # Use CoffeeScript for .coffee assets and views gem 'coffee-rails', '~> 4.2' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.5' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use ActiveStorage variant # gem 'mini_magick', '~> 4.8' # Use Capistrano for deployment # gem 'capistrano-rails', group: :development ######### 追加したのはココ########## gem 'sorcery' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.1.0', require: false group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15', '< 4.0' gem 'selenium-webdriver' # Easy installation and use of chromedriver to run system tests with Chrome gem 'chromedriver-helper' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
ターミナルで
bundle
として、bundle install…
rbenv: bundle: command not found The `bundle' command exists in these Ruby versions: 2.2.3
おっと、こんなエラーが出た場合は…
gem install bundler
を実行してから再度bundle installする。
無事にインストールが完了する。
その後
rake db:create
でデータベースを作成。
sorceryの導入
ターミナルで
rails g sorcery:install
create config/initializers/sorcery.rb generate model User --skip-migration invoke active_record create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml insert app/models/user.rb insert app/models/user.rb
こんな感じでファイルが生成されるが何か足りない…
マイグレファイルなくね!?
調べたところ、sorceryがrailsの最新版(この時点で5.2)に対応していなかったらしく、githubから最新版を指定してbundle installする必要があるとのこと。
先程のGemfileのsorceryの箇所を、
gem 'sorcery', github: 'sorcery/sorcery'
にして、先程生成したファイルを削除。
rails d sorcery:install
すると、
remove config/initializers/sorcery.rb generate model User --skip-migration subtract app/models/user.rb subtract app/models/user.rb
となるので、再度
rails g sorcery:install
create config/initializers/sorcery.rb
generate model User --skip-migration
invoke active_record
identical app/models/user.rb
invoke test_unit
identical test/models/user_test.rb
identical test/fixtures/users.yml
insert app/models/user.rb
insert app/models/user.rb
create db/migrate/20181004061537_sorcery_core.rb
マイグレファイルが無事作成された。
【db/migrate/20181004061537_sorcery_core.rb】
class SorceryCore < ActiveRecord::Migration[5.2] def change create_table :users do |t| t.string :email, :null => false t.string :crypted_password t.string :salt t.timestamps :null => false end add_index :users, :email, unique: true end end
問題なければ
rake db:migrate
※Userテーブルの項目を変更したい場合はmigrateファイルを変更してから
User登録機能の作成
scaffoldで一気にファイル生成
ターミナルで
rails g scaffold user email:string crypted_password:string salt:string --migration false
とすると、必要なファイル(必要ないファイルも)が一気に生成される。
フォームのcrypted_passwordとsaltの項目名を変更し、text_fieldからpassword_fieldに。
【view/users/_form.html.erb】
<div class="field"> <%= form.label :password %> <%= form.password_field :password %> </div> <div class="field"> <%= form.label :password_confirmation %> <%= form.password_field :password_confirmation %> </div>
また、ストロングパラメーターも更新しておく必要がある。
【controllers/users_controller.rb】
def user_params params.require(:user).permit(:name, :email, :gender, :age, :password, :password_confirmation) end
バリデートの設定
【models/user.rb】
class User < ActiveRecord::Base authenticates_with_sorcery! validates :name, :email, :gender, :age, presence: true validates :password, length: { minimum: 4 }, if: -> { new_record? || changes[:crypted_password] } validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] } validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] } validates :email, uniqueness: true end
http://localhost:◯◯◯◯/usersにアクセスするとUserを作れるようになっている。
認証機能(ログイン)
Userの操作ができたので、次はログイン機能を実装。
ターミナルで
rails g controller UserSessions new create destroy
を実行。
ログイン・ログアウトに必要なファイルが一気に生成される。
controllerの実装
こんな感じで書き込む。
【controllers/user_session_controller.rb】
class UserSessionsController < ApplicationController def new @user = User.new end def create if @user = login(params[:email], params[:password]) redirect_back_or_to(:users, notice: 'Login successful') else flash.now[:alert] = 'Login failed' render action: 'new' end end def destroy logout redirect_to(:users, notice: 'Logged out!') end end
viewの実装
app/views/user_sessions内のcreate.html.erbとdestroy.html.erbは要らないので削除。
new.html.erbにログインフォームを実装。
【views/user_sessions/new.html.erb】
<h1>Login</h1> <%= form_tag login_path, method: :post do %> <div class="field"> <%= label_tag :email %><br /> <%= text_field_tag :email %> </div> <div class="field"> <%= label_tag :password %><br /> <%= password_field_tag :password %> </div> <div class="actions"> <%= submit_tag "Login" %> </div> <% end %>
Routesの設定
【config/routes.rb】
Rails.application.routes.draw do resources :users get '/login' => 'user_sessions#new', as: :login post '/login' => 'user_sessions#create' delete '/logout' => 'user_sessions#destroy', as: :logout root 'user_sessions#new' end
ヘッダーにナビゲーションボタンを設置
【views/layout/application.html.erb】
<!DOCTYPE html> <html> <head> <title>SportCircle</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <div id="nav"> <% if current_user %> <%= link_to "Edit Profile", edit_user_path(current_user.id) %> <%= link_to "Logout", :logout, method: :post %> <% else %> <%= link_to "Register", new_user_path %> | <%= link_to "Login", :login %> <% end %> </div> <div> <p id="notice"><%= flash[:notice] %></p> <p id="alert"><%= flash[:alert] %></p> </div> <%= yield %> </body> </html>
before_actionでログインフィルターをかける
ログインしていない場合はコンテンツを見れないようにする。
逆に、アカウント登録やログイン画面などはログアウトしている状態でないといけない。
【controllers/application_controller.rb】
class ApplicationController < ActionController::Base before_action :require_login end
【acontrollers/users_controller.rb】
class UsersController < ApplicationController skip_before_action :require_login, only: [:index, :new, :create] end
【controllers/user_sessions_controller.rb】
class UserSessionsController < ApplicationController skip_before_action :require_login, except: [:destroy] end
URL直打ちアクセスの防止
ログインしていない状態のときにURL直打ちで直接アクセスされるとページが見れてしまうので、
application_controllerにプライベートメソッドを追加。
【controllers/application_controller.rb】
private def not_authenticated redirect_to login_path, alert: "Please login first" end
loginやcurrent_userが使えない!
ブラウザで確認したところ、current_userでエラー。
current_userの箇所をコメントアウトしてログインを試してみてもloginでエラー。
なんで???
詳しい原因はわからないが、githubから呼んでいるsorceryが新しすぎる?らしい。
Gemfileのgem 'sorcery', github: 'sorcery/sorcery'
をコメントアウトして、代わりにgem 'sorcery'
でbundle installしたら問題なく動いた。
まとめ
- 導入はかなり簡単!
- 公式チュートリアル(Simple Password Authentication · Sorcery/sorcery Wiki · GitHub)が分かりやすい
- シンプルなログイン機能ならオススメ
- バージョンには気をつける