Railsからはじめるプログラミング日記

駆け出しプログラマー(主にRuby on Rails)の業務で役立ったコードをメモしていくブログです。自分だけでなく誰かの役に立てれば本望です。

【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日と月末の日付と曜日を求め、次のような形式でカレンダーを表示させてください。

f:id:oe526:20181025164517p:plain

やってみた

自力で解くのは無理でした。

ひとまず解き方を理解するのに公式の回答を見ました…なんと「万年カレンダー」の原理を使った回答でした。

…万年カレンダーってなに?って感じだったので諦めて他の回答をネット上で探すことに。

しかしあまり数はないようですね…結局「プロを目指す人のためのRuby入門」の著者、伊藤淳一さんが回答をブログに載せていたので参考にすることに。

blog.jnito.com

私的回答

会社の先輩にも手伝ってもらいながら出した回答が以下です!

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によると、主要ブラウザは軒並み対応してますね。
ただ、スマートフォンのブラウザはまだ怪しい感じです。 f:id:oe526:20181022095259p:plain

基本的な使い方

ボックスの右下に斜めの線が表示されていると思うので、ドラッグすると好きなサイズに調整できます。

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でエラー。 f:id:oe526:20181004173720p:plain

current_userの箇所をコメントアウトしてログインを試してみてもloginでエラー。 f:id:oe526:20181004173725p:plain

なんで???

詳しい原因はわからないが、githubから呼んでいるsorceryが新しすぎる?らしい。
Gemfileのgem 'sorcery', github: 'sorcery/sorcery'をコメントアウトして、代わりにgem 'sorcery'でbundle installしたら問題なく動いた。

まとめ

筆者オススメ技術書