20200728のRailsに関する記事は25件です。

Rails メールアドレス、パスワード+αでログイン認証する

目的

nation_nameプロパティをログインに必要な項目に追加する。

使い方

  • カラムとして存在しないプロパティを追加するattr_accessorメソッドを使ってnation_nameプロパティをユーザーモデルに追加する
user.rb
class User < ApplicationRecord
  #accessor
  attr_accessor :nation_name
  • registration/new.html.erbとsessions/new.html.erbに以下のコードを追記する。
:new.html.erb
<div class="field">
    <%= f.label :国名 %><br />
    <%= f.text_field :nation_name, autofocus: true %>
  </div>
  • パラメーターとして送られるgroup_nameが許可されるようにする
:application.controller
class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  before_action :configure_permitted_parameters, if: :devise_controller?

  def configure_permitted_parameters
    #sign_in時にnation_nameも許可する
    devise_parameter_sanitizer.permit(:sign_in, keys:[:nation_name])
    #sign_up時にnation_nameも許可する
    devise_parameter_sanitizer.permit(:sign_up, keys:[:nation_name])
    #account_update時にnation_nameも許可する
    devise_parameter_sanitizer.permit(:account_update, keys:[:nation_name])
  end 
end

  • ユーザーの新規登録時にネーションIDが保存されるようにする
user.rb
class User < ApplicationRecord
  #accessor
  attr_accessor :nation_name
  #association
  belongs_to :nation
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable,
          authentication_keys: [:email, :nation_name]

  #validation
  before_validation :nation_name_to_id, if: :has_nation_name?

  def self.find_first_by_auth_conditions(warden_conditions)
    conditions = warden_conditions.dup
    nation_name = conditions.delete(:nation_name)
    nation_id = Nation.where(name: nation_name).first
    email = conditions.delete(:email) || conditions.delete(:unconfirmed_email)

    # devise認証を、複数項目に対応させる
    if nation_id && email
      find_by(nation_id: nation_id, email: email)
    elsif conditions.has_key?(:confirmation_token)
      where(conditions).first
    elsif email
      where(email: email).first
    else
      false
    end
  end

  private
  def has_nation_name?
    nation_name.present?
  end

  def nation_name_to_id
    nation = Nation.where(name: nation_name).first_or_create
    self.nation_id = nation.id
  end

end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自作ログイン機能

事前情報

セッション

「サーバ側に用意された一つのブラウザから連続しておくれらている一連のリクエスト間で「状態」を共有できる仕組み」のことを言う
セッションがなければ、同じユーザから送られた一つ目のリクエストから二つ目のリクエストに情報を受け渡すことができない

controller
#セッションにアクセス
session[:user_id] = @user.id

#セッションの値の取り出し
@user.id = session[:user_id]

Cookie

ブラウザとwebサーバの間での情報の受け渡し。
「複数のリクエストの間で共有したい「状態」をブラウザ側に保存する仕組み」

Userモデルを作る

rails g model user name:string email:string password_digest:string
#同じパスワードから生成すると同じだが、それ自体は無意味な文字列
migrationfile
class CreateUsers < ActiveRecord::Migration[5.2]
 def change
  create_table :users do |t|
   t.string :name, null: false
   t.string :email, null: false
   t.string :password_digest, null: false

   t.timestamps
   t.index :email, unique: true
  end
 end
end
rails db:migrate

パスワードをdigestに変換する仕組みを作る

has_secure_passwordを使用し、パスワードをハッシュ化するためbcryptと言うハッシュ関数を提供するgemをインストールする

Gemfile
gem 'bcrypt'
#記述あるはずなのでコメントアウトをする
bundle install
model
class User < ApplicationRecord
 has_secure_password
end
#この一行で「password」「password_confirmation」 カラム が追加される

adminフラグのカラムをuserモデルに追加する

rails g migration add_admin_to_users
migrationfile
class AddAdminToUsers < ActiveRecord::Migration[5.2]
 def change
  add_column :user, :admin, :boolean, default: false, null: false
 end
end
rails db:migrate

コントローラの作成

rails g controller Admin::Users new edit show index
#Adminと言うモジュールの名前空間の中にUsersControllerクラスを定義する

ルーティングの設定

routes
Rails.application.routes.draw do
 namespace :admin do 
  resources :users
 end
 root to: 'task#index'
 resources :tasks
end

コントローラの中身を定義

controller
class Admin::UsersController < ApplicationController
 def index
  @users = User.all
 end

 def show
  @user = User.find(params[:id])
 end

 def new
  @user = User.new
 end

 def edit
  @user = User.find(params[:id])
 end

 def create
  @user = User.new(user_params)

  if @user.save
   redirect_to admin_user_url(@user), notice:"ユーザー「#{user.name}」を登録しました。"
  else
   render :new
  end
 end

 def update
  @user = User.find(params[:id])

  if @user.update(user_params)
   redirect_to admin_user_url(@user), notice:"ユーザー「#{user.name}」を更新しました。"
  else
   render :edit
  end
 end

 def destroy
  @user = User.find(params[:id])
  @user.destroy
  redirect_to admin_user_url(@user), notice:"ユーザー「#{user.name}」を削除しました。"
 end

 private

 def user_params
  params.require(:user).permit(:name, :email, :admin, :password, :password_confirmation)
 end
end

モデルの中身を定義

model
class User < ApplicationRecord
 has_secure_password

 validates :name, presence: true
 validates :email, presence: true, uniqueness: true
end

ビューの中身を定義

index
h1 ユーザー一覧
= link_to '新規登録', new_admin_user_path, class: 'btn btn-primary'

.mb-3
 table.table.table-hover
  thead.thead-default
   tr
    th= User.human_attribute_name(:name)
    th= User.human_attribute_name(:email)
    th= User.human_attribute_name(:admin)
    th= User.human_attribute_name(:created_at)
    th= User.human_attribute_name(:updated_at)
    th
 tbody
  - @users.each do |user|
    tr
     td= link_to user.name, [:admin, user]
     td= user.email
     td= user.admin? ? 'あり' : 'なし'
   td= user.created_at
     td= user.updated_at
     td
      = link_to '編集', edit_admin_user_path(user), class:'btn btn-primary mr-3'
      = link_to '削除', [:admin, user], method: :delete, date:{confirm:ユーザー「#{user.name}」を削除します。よろしいですか?”}, class:'btn btn-danger'

new
h1 ユーザー登録
.navjustify-content-end
 = link_to '一覧', admin_users_path, class: 'nav-link'

= render partial: 'form', locals:{user: @user}
edit
h1 ユーザー編集
.navjustify-content-end
 = link_to '一覧', admin_users_path, class: 'nav-link'

= render partial: 'form', locals:{user: @user}
_form
- if user.errors.present?
  ul#error_explanation
   - user.errors.full_messages.each do |message|
     li = message
= form_with model:[:admin, user], local: true do |f|
  .form-group
   = f.label :name, '名前'
   = f.text_field :name, class: 'form-control'
  .form-group
   = f.label :email, 'メールアドレス'
   = f.text_field :email, class: 'form-control'
  .form-check
   = f.label :admin, class: 'form-check-label' do
   = f.check_box :admin, class: 'form-check-input'
   | 管理者権限
  .form-group
   = f.label :password, 'パスワード'
   = f.text_field :password, class: 'form-control'
  .form-group
   = f.label :password_confirmation, 'パスワード(確認)'
   = f.text_field :password_confirmation, class: 'form-control'
 = f.submit '登録する', class: 'btn btn-primary'
show
h1 ユーザーの詳細

.nav.justify-content-end
 = link_to '一覧', admin_users_path, class: 'nav-link'
table.table.table-hover
 tbody
  tr
   th= User.human_attribute_name(:id)
   td= @user.id
  tr
   th= User.human_attribute_name(:name)
   td= @user.name
  tr
   th= User.human_attribute_name(:email)
   td= @user.email
  tr
   th= User.human_attribute_name(:admin)
   td= @user.admin? ? 'あり' : 'なし'
  tr
   th= User.human_attribute_name(:created_at)
   td= @user.created_at
  tr
   th= User.human_attribute_name(:updated_at)
   td= @user.updated_at
= link_to '編集', edit_admin_user_path, class:'btn btn-primary mr-3'
= link_to '削除', [:admin, user], method: :delete, date:{confirm:ユーザー「#{user.name}」を削除します。よろしいですか?”}, class:'btn btn-danger'

ja.yml
ja:
 activerecord:


   attributes:
    task:

    user:
     name: 名前
   email: メールアドレス
   admin: 管理者権限
   password: パスワード
     password_confirmation: パスワード(確認)
     created_at: 登録日時
     updated_at: 更新日時 

ログインフォームの表示

rails g controller Sessions new
routes
Rails.application.routes.draw do
 get '/login', to:'sessions#new'

end
new
h1 ログイン
= form_with scope: :session, local: true do |f|
  .form-group
   = f.label :email, 'メールアドレス'
   = f.text_field :email, class: 'form-control', id: 'session_email'
  .form-group
   = f.label :password, 'パスワード'
   = f.text_field :password, class: 'form-control', id: 'session_password'
 = f.submit 'ログインする', class: 'btn btn-primary'

ログインの実装

routes
Rails.application.routes.draw do
 get '/login', to:'sessions#new'
 post '/login', to:'sessions#create'

end
sessions_controller
class SessionsController < ApplicationController
 def new
 end

 def create
  user = User.find_by(email: session_params[:email]) #メールアドレスで検索

  if user&.authenticate(session_params[:password]) #パスワード認証 見つからなかった場合はnilで返したいから&.
   session[:user_id] = user.id #セッションにuser_idを格納 
   redirect_to root_url, notice: 'ログインしました。'
  else
   render :new
  end
 end

 private
 def session_params
  params.require(:session).permit(:email, :password)
 end
end

ログイン後、ユーザーを取得したい場合、、

以下でコードで取得できるが、ヘルパーメソッドを定義する方が良い

User.find_by(id: session[:user_id)
application_controller
class ApplicationController < ActionController::Base
 helper_method :current_user

 private

 def current_user
  @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
 end
end

ログアウトを実装

routes
Rails.application.routes.draw do
 get '/login', to:'sessions#new'
 post '/login', to:'sessions#create'
 delete '/logout', to:'sessions#destroy'
end
sessions_controller
class SessionsController < ApplicationController
 def new
 end

 def create
 ・・・
 end

 def destroy
  reset_session
  redirect_to root_url, notice: 'ログアウトしました。'
 end

 private
 ・・・
end
application
 ul.navbar-nav.ml-auto
  - if current_user
    li.nav-item= link_to 'タスク一覧', tasks_path, class:'nav-link'
    li.nav-item= link_to 'ユーザー一覧', admin_users_path, class:'nav-link'
    li.nav-item= link_to 'ログアウト', logout_path, method: :delete, class:'nav-link'
  - else
    li.nav-item= link_to 'ログイン', login_path, class:'nav-link

各アクションの実行前に毎回ユーザーがログインしているか調べる

application_controller
class ApplicationController < ActionController::Base
 helper_method :current_user
 before_action :login_required

 private

 def current_user
  @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
 end

 def login_required
  redirect_to login_url unless current_user
 end
end

ログイン画面のときは、「before_action :login_required」がスキップされるように

sessions_controller
class SessionsController < ApplicationController
 skip_before_action :login_required
end

UserとTaskを紐付ける

rails g migration AddUserIdTasks
migrationfile
class AddUserIdToTasks < ActiveRecord::Migration[5.2]
 def up
  execute 'DELETE FROM tasks;' #外部キーのついていない今までのtaskが削除される
  add_reference :tasks, :user, null: false, index: true
 end

 def down
  remove_reference :tasks, :user, index:true
 end
end

アソシエーションを定義する

user.rb
class User < ApplicationRecord
 validates :email, presence: true

 has_many :tasks
end
task.rb
class Task < ApplicationRecord
 validates :name, presence: true
 validate :validate_name_not_including_comma

 belongs_to :user

end

紐付け後コントローラーの修正

tasks_controller
 def index
  @tasks = Task.all #を①または②に変更
  @tasks = current_user.tasks #①
  @tasks = Task.where(user_id: current_user.id) #②
 end

 def create
  @task = Task.new(task_param)#を①または②に変更
  @task = Task.new(task_param.merge(user_id: current_user.id)) #①
  @task = current_user.tasks.new(task_params) #②
  if @task.save
   redirect_to @task, notice: "タスク「#{@task.name}」を登録しました。"
 else
  render :new: 
 end

  Task.find(params[:id])#を以下に変更
 current_user.tasks.find(params[:id])

end

管理者だけに管理機能を表示させる

application
 ul.navbar-nav.ml-auto
  - if current_user
    li.nav-item= link_to 'タスク一覧', tasks_path, class:'nav-link'
    - if current_user.admin?
      li.nav-item= link_to 'ユーザー一覧', admin_users_path, class:'nav-link'
    li.nav-item= link_to 'ログアウト', logout_path, method: :delete, class:'nav-link'
  - else
    li.nav-item= link_to 'ログイン', login_path, class:'nav-link
users_controller
class Admin::UsersController < ApplicationController
 before_action :require_admin

 private
 def require_admin
  redirect_to root_url unless current_user.admin?
 end
end

参考|現場で使える Ruby on Rails 5速習実践ガイド

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

form_for

使い方

メソッド

f.textfield :インスタンスのカラム名

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails form_for フォームコントロール

使い方

メソッド

f.textfield :インスタンスのカラム名

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpecのCapybaraを使ったシステムスペックで`NotImplementedError`が出たときの対処法

システムスペックを書いている際にCapybaraのsend_keysメソッドを使おうとしたら、
NotImplementedErrorが発生し、解決に時間がかかってしまったので、対処法を備忘録として記載します。

結論から言うと、RSpecでJavaScriptを使う設定をし忘れていたことが原因でした。

エラーの発生

下記のテスト実行時に、NotImplementedErrorが発生しました。

Failures:

  1) Messages can display messages in real time
     Failure/Error: find('#message').send_keys(:enter)

     NotImplementedError:
       NotImplementedError

発生箇所はsend_keysメソッドのようです。

spec/systems/messages_spec.rb
require 'rails_helper'

RSpec.describe 'Messages', type: :system do
  # 略
  it 'can display messages in real time', js: true do
    log_in(user)
    visit room_path(room)
    message = 'Hello'
    expect{
      fill_in 'Write a message', with: message
      # send_keysメソッドでエラー発生
      find('#message').send_keys(:enter)
    }.to change(Message, :count).by(1)
    expect(page).to have_content message
  end

対処法

1 JSのドライバの設定を追加する

spec/support/capybara.rb
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :rack_test
  end
  # 追記
  config.before(:each, type: :system, js: true) do
    driven_by :selenium_chrome_headless
  end
end

2 スペックでJSを有効にする

spec/support/capybara.rb
require 'rails_helper'

RSpec.describe 'Messages', type: :system do
  # 略
  # js: true を追記
  it 'can display messages in real time', js: true do
    log_in(user)
    visit room_path(room)
    message = 'Hello'
    expect{
      fill_in 'Write a message', with: message
      find('#message').send_keys(:enter)
    }.to change(Message, :count).by(1)
    expect(page).to have_content message
  end

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Bootstrap導入手順をまとめてみた(RubyonRails)

初めまして!
弱弱駆け出しエンジニアのてしまと申します。

初投稿です!

今回はRubyonRailsでのBootStrapの導入手順についてまとめてみました。
導入自体は簡単です!
もし僕のような初学者でBootstrapに興味がある方は参考になればと思います。
実際に使った例も載せているため後半は長くなっております。

内容

①RubyonRailsにBootstrapを導入
②Bootstrapを使ってテーブルとボタンの装飾

対象者

・RubyonRailsで簡単なアプリケーションを立ち上げたことがある
・Bootstrapを知らないor使ってみたいと考えている
・Scaffoldを使ったことがある(無くても導入はできます)

Bootstrapとは

CSSの「フレームワーク」
通常CSSを書く場合、全てのスタイルを自分で作っていく必要がありますが
このフレームワークにはよく使われるスタイルがあらかじめ定義してあるので
ルールに沿って利用するだけで整ったデザインのページを作成できます。
(引用:https://techacademy.jp/magazine/6270)

色々調べているとBootstrapをダウンロードすると出てきましたが
今回、Gemを使ったので特にダウンロードせずに使えました。

前提条件

今回はBootstrapの導入と一部使用例をまとめています。
予めscaffoldを使ってtasksテーブルを作成。titleカラム、textカラムを追加しております。
今回使いませんがdeviseも入れてます。

では早速本題の導入手順です。

RubyonRailsでBootstrapを使えるようにする

①Gemの導入

まずはGemfileにgemを導入します。

Gemfile.
gem 'bootstrap', '~> 4.1.1'
gem 'jquery-rails'

jqueryは今回使っていませんが、いつか使うと思われるので入ってなければ記述。

ターミナル.
bundle install

bundle installも忘れずに。

②SCSSファイルにimport

application.cssをapplication.scssに名前を変更。
そして以下の文を追記

application.scss
@import "bootstrap";

一応application.jsにも追記

application.js
//= require bootstrap

はい!以上で準備完了です!
これだけでBootstrapが使えるようになります!
肝心の使い方があまり載ってなかったので
実際にコードを入れて装飾してみたいと思います。

Bootstrapのコードを実際に入れてみる

では実際にBootstrapを使って装飾をしていきます。
装飾前がこんな感じです。
Scaffoldで生成してるのである程度形は整ってます。
スクリーンショット 2020-07-28 15.26.59.png

①まずはHPにアクセス

「Bootstrap HP」
https://getbootstrap.com/

スクリーンショット 2020-07-28 13.31.57.png

ヘッダー左2番目のDocumentationをクリックして参考コードを検索しに行きます。

②作りたいCSSを検索

スクリーンショット 2020-07-28 13.32.12.png
今回はテーブルを作るので「table」で検索
するとサンプル画像とそのコード一覧が出てくるので使いたいものを選びコピペするだけです!

③使いたいコードをコピー

一覧は画面の大きさの都合上、割愛してます。
今回は以下の画像サンプルのコードを使って実装していきます。

スクリーンショット 2020-07-28 13.57.09.png

③エディタに貼り付け

先ほどコピーしたコードをエディタの対象ファイルに貼り付けます。
今回はviews/tasks(ご自身のファイル名)/index.html.erb
一旦、一番下などに貼り付けしてしっかり反映するか確かめてみると良いです。
下記はコピーして貼り付けただけです。
このテーブルタグの中身の記述(白テキスト部分)を自分のデータに置き換えていきましょう!

<table class="table table-striped">
  <thead>
    <tr>
      <th scope="col">#</th>
      <th scope="col">First</th>
      <th scope="col">Last</th>
      <th scope="col">Handle</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">1</th>
      <td>Mark</td>
      <td>Otto</td>
      <td>@mdo</td>
    </tr>
    <tr>
      <th scope="row">2</th>
      <td>Jacob</td>
      <td>Thornton</td>
      <td>@fat</td>
    </tr>
    <tr>
      <th scope="row">3</th>
      <td>Larry</td>
      <td>the Bird</td>
      <td>@twitter</td>
    </tr>
  </tbody>
</table>

④記述場所にデータを置き換え(index.html.erbファイル)

貼り付け前のファイルの記述がこちらです。(参考までに)
views/tasks/index.html.erb

 <p id="notice"><%= notice %></p>
<h1>Tasks</h1> 
<table> 
   <thead>
     <tr>
       <th>Title</th>
     <th>Text</th>
       <th colspan="3"></th>
     </tr>
   </thead> 
   <tbody>
     <% @tasks.each do |task| %>
       <tr> |
         <td><%= task.title %></td>
         <td><%= task.text %></td>
         <td><%= link_to 'Show', task %></td>
         <td><%= link_to 'Edit', edit_task_path(task) %></td>
         <td><%= link_to 'Destroy', task, method: :delete, data: { confirm: 'Are you sure?' } %></td>
     </tr>
     <% end %> 
 </tbody> 
</table>
<br>
<%= link_to 'New Task', new_task_path %> 

Bootstrapのサンプルに余計なテーブルがあるので削除。
先ほどコピペしたコードのテーブルタグの中身を表示したいデータの記述に置き換えます。
バーっと作ったのでインデントなど細かいところはすみません?
変更場所は見比べていただければと思います。
貼り付けがうまくいったら元々あった記述は消しちゃいましょう。   
以下が変更後の記述です。

<h1>Tasks</h1>
<% if user_signed_in?%>
  <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
<% else %>
  <%= link_to "新規登録", new_user_registration_path %>
  <%= link_to "ログイン", new_user_session_path %>
<% end %>
<%# ----------以下がBootstrapのテーブル---------- %>
<table class="table table-striped">
  <thead>
    <tr>
      <th scope="col">No</th>
      <th scope="col">Title</th>
      <th scope="col">Text</th>
      <th scope="col">Date</th>

      <th scope="col">Show</th>
      <th scope="col">Edit</th>
      <th scope="col">Destroy</th>
    </tr>
  </thead>
  <tbody>
      <% @tasks.each.with_index do |task, no| %>
        <tr>
          <td><%= no++1 %></td>
          <td><%= task.title %></td>
          <td><%= task.text %></td>

          <td><%= task.created_at.strftime('%Y/%m/%d %H:%M')  %></td>
          <td><%= link_to 'Show', task %></td>
          <td><%= link_to 'Edit', edit_task_path(task) %></td>
          <td><%= link_to 'Destroy', task, method: :delete, data: { confirm: 'Are you sure?' } %></td>
        </tr>
      <% end %>
    </tbody>
</table>
<%# ----------以上がBootstrapのテーブル---------- %>
<br>
<%= link_to 'New Task', new_task_path %> 

完成図と相違があると困惑するため
deviseで使っている上の部分の記述があります。
気になさらずに。
コメントアウトで区切っている中身のみをご確認ください。

⑤ボタンも作成

せっかくなのでボタンも入れてみましょう!
今度は「btn」で検索して(検索方法は色々あると思います)
スクリーンショット 2020-07-28 14.04.50.png
以上のように色々出てきました。
今回は新規投稿ページにリンクさせたいので画像最下部のaタグのCSSを拝借。

まずは以下のリンクの記述を削除し

<%# ----------以上がBootstrapのテーブル---------- %>
<br>
<%= link_to 'New Task', new_task_path %> 

以下のように書き換え
※引用元のa href=後ろのリンク先の記述とタグ内の文字を変更しただけ
引用元に合わせてPrefixからURIパターンに書き換えてます。

<br>
<a href="tasks/new" class="btn btn-primary btn-lg active" role="button" aria-pressed="true">New Task</a>

完成画像

スクリーンショット 2020-07-28 13.34.25.png

HTMLの記述だけでサンプル画像と同じ見た目の装飾ができました!
(CSSのクラス名がBootstrapによって決められているため)
この高さの揃った綺麗なテーブルを作るのが意外と大変•••
きっと初学者の方なら共感いただけるかと思います。

Bootstrapを使えば決まったレイアウトにデータを置き換えるだけなので
このように簡単にCSSを作成可能!
アイコンなどもたくさんあったのでFontawsome派の方もぜひみる価値はありそうです!
今後はJSなども色々試してみてみたいと思います!

参考資料

初めてのRuby on rails Bootstrap導入編 [Memo for neko]
https://qiita.com/Matteneko3/items/4dae9e55054e4a4affb4

参考資料の方の記事を長く書いただけの記事です、、、
画像もつけて結構丁寧に書いたつもりですが
初投稿なのでもし間違っている点、ご意見などございましたら
コメントに残していただけると勉強、励みになります?‍♂️?
よろしくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails][Turbolinks] inline Javascript が実行されない 解決法

Javascript周り の取り扱いが難しい Turbolinks

起こったこと

ページ毎に任意のJavascript を実行したいが
普通に書いたら以下エラー。

ページ遷移でも同様。

ChromeDeveloperConsole
Refused to execute inline script because it violates the following Content Security Policy directive: 
"script-src 'self' https: 'unsafe-inline' 'nonce-UiVx2CiP0HHN9jOOSEG43g=='". 
Note that 'unsafe-inline' is ignored if either a hash or nonce value is present in the source list.

Content-Security-Policy (CSP) (Cross Site Scripting (XSS) や data injection 攻撃を防ぐための HTTP の仕様)のために
ブラウザでの Javascript 実行が制限されている。

https://qiita.com/tearoom6/items/30e3aacaa432860d4b36

解決法

Rails 側で nonce を発行し、Javascript を認証する。
(HTTPヘッダに追加)

views/layouts/head.html.slim
= csp_meta_tag
config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy_nonce_generator = -> (request) do
  # use the same csp nonce for turbolinks requests
  if request.env['HTTP_TURBOLINKS_REFERRER'].present?
    request.env['HTTP_X_TURBOLINKS_NONCE']
  else
    SecureRandom.base64(16)
  end

script タグに nonce をつける

some_page.html.slim
= javascript_tag, nonce: true
 | console.log('JS here');

Turbolinks のEvent に合わせて、nonce を入れ替える。

application.js
document.addEventListener('turbolinks:request-start', function(event) {
  var nonceTag = document.querySelector("meta[name='csp-nonce']");
  if (nonceTag) event.data.xhr.setRequestHeader('X-Turbolinks-Nonce', nonceTag.content);
});
document.addEventListener('turbolinks:before-cache', function() {
  Array.prototype.forEach.call(document.querySelectorAll('script[nonce]'), function(element) {
    if (element.nonce) element.setAttribute('nonce', element.nonce);
  });
});

Turbolinks Event list

https://github.com/turbolinks/turbolinks#full-list-of-events

参考

https://github.com/turbolinks/turbolinks/issues/430

https://discuss.rubyonrails.org/t/turbolinks-broken-by-default-with-a-secure-csp/74790

https://github.com/thredded/thredded/pull/797/commits/beac77f9c7ac0c880afac3302f752157f2945afe

https://stackoverflow.com/questions/57044770/what-is-csp-meta-tag-in-rails-5-2-3-and-how-does-it-work

https://y-yagi.hatenablog.com/entry/2018/02/24/070139

https://site-builder.wiki/posts/1050

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コールバック(モデルの状態を自動的に制御)

イベントの前後に任意の処理を挟むことをコールバックと言う
主に、イベントの前(before)、イベントの後(after)、イベントを挟む(around)のタイミングで書くことができる。

※トランザクション
一連の複数の処理によるデータベースの整合性を保つための機能。
コーツバックの一つで例外が発生した場合、ロールバックと言う取り消し作業が発生し、その後のコールバックが実行されない仕組みになっている。

コールバックの種類 使い道
before_validation 検証前の値の正規化
after_validation 検証結果(エラーメッセージ)の加工
before_save, before_create, before_update saveのために裏側で行いたいデータ準備を行う
after_save, after_create, after_update そのモデルの状態に応じてほかのモデルの状態をカエルなど、連動した挙動を実現する。検証エラーを出してもユーザーにはどうすることもできない状態異常を防ぐために例外を出す
before_destroy 削除してOKかチェックし、ダメなら例外を出すなどして防ぐ
after_destroy そのモデルの削除に応じてほかのモデルの状態をかえるといった連動した挙動を実現する

参考|現場で使える Ruby on Rails 5速習実践ガイド

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

form_withがUndefinedエラーになったので、railsのバージョンをあげたらrails sが立ち上がらなくなった話

railsで入力フォームを作ろうをしたら

undefined method `form_with' for #<#Class:0x00007fdef2555c48:0x00007fdef1211178>
Did you mean? form_tag

とエラー。

どうやらform_withはrails5.1以降の機能だったらしい。

なのでrailsのバージョンをあげることに

Gemfile
gem 'rails', '5.2.1'

そして bundle install

無事にバージョンアップに成功!

しかし rails s したところ

undefined method `halt_callback_chains_on_return_false=' for ActiveSupport:Module (NoMethodError)

と言われる。
どうやらhalt_callback_chains_on_return_falseはRails 4との後方互換のためのメソッドで、定義されていないメソッドだと言われる。
なのでターミナルにて

ターミナル
vi config/initializers/new_framework_defaults.rb
config/initializers/new_framework_defaults.rb
ActiveSupport.halt_callback_chains_on_return_false = false

をコメントアウトでok!

無事にrails s 立ち上がりました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Couldn't find ModelName with 'id'=batch_action

Rails 4.2.0
activeadmin (1.0.0.pre1)

active_adminで
タイトルのようなエラーが出ました。

解決方法

app/admin/model_name.rb
ActiveAdmin.register ModelName do
  #これをかく
  config.batch_actions = false
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] テーブルの新規作成するコマンド

rails g model テーブル名 カラム名:型
テーブル名は頭文字は大文字で、単数形にする
カラム名と型は省略可能

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] テーブルの新規作成

rails g model テーブル名 カラム名:型
テーブル名は頭文字は大文字で、単数形にする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] cloneしたリポジトリをrailsサーバーで起動する

git hubからcloneしたリポジトリをrailsサーバーで起動するまでの道のりを記録しました。

リポジトリをclone

$ git clone https://github.com/*****/*****.git

サーバー起動コマンドを打ってみる

$ bin/rails s

Could not find rake-13.0.1 in any of the sources
Run `bundle install` to install missing gems.

エラーが出て起動しません。bundleがないみたい。指定されたコマンドを打ってみます。

$ bundle install

/Users/*****/.anyenv/envs/rbenv/libexec/rbenv-exec: /usr/local/bin/bundle: /System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/bin/ruby: bad interpreter: No such file or directory
/Users/*****/.anyenv/envs/rbenv/libexec/rbenv-exec: line 47: /usr/local/bin/bundle: Undefined error: 0

これもまたエラーが出て失敗しています。

エラー解消の旅

teratailCould not find XXXX in any of the sources エラーの修正方法を参考にさせていただきました。

まずはどのruby, bundleを参照しているか確認。

$ which ruby

/Users/*****/.anyenv/envs/rbenv/shims/ruby

$ which bundle

/Users/*****/.anyenv/envs/rbenv/shims/bundle

.anyenvの中のrubyとbundleを見ているのでこれは大丈夫そう。

そして、bundlerをインストールする。
$ gem install bundler
You don't have write permissionsと言われたのでsudoをつけます。

$ sudo gem install bundler
途中2回PC起動時のパスワードの入力を求められますがbundlerのインストールに成功。

満を辞して$ bin/rails s

=> Booting Puma
=> Rails 6.0.2.2 application starting in development 
=> Run `rails server --help` for more startup options
error Couldn't find an integrity file                                          
error Found 1 errors.                                                          


========================================
  Your Yarn packages are out of date!
  Please run `yarn install --check-files` to update.
========================================
(以下省略)

yarnをinstallしてね、とのこと。コマンドまで教えてくれていたのに癖で--check-filesオプションを付けずに、$ yarn installをしてしまいましたが問題ありませんでした。

(省略)
✨  Done in 17.93s.

install成功!

そして今度こそ!!
$ bin/rails s

=> Booting Puma
=> Rails 6.0.2.2 application starting in development 
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 4.3.3 (ruby 2.6.3-p62), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000
* Listening on tcp://[::1]:3000
Use Ctrl-C to stop

railsサーバー無事起動しました^^

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

letを使用することで、通るはずのテストが通らないfalse Alarmを生じる可能性がある

先日RSpecのテストコードにレビューを頂いたので、

なるべく多くを学びたいと思い、その内容を咀嚼する過程での気付きを書きたいと思います

元記事
RSpecのEmail一意性テストで"Email has already been taken"問題と回避策 - Qiita

  • なるべく小さいscope?(contextの中など)を見ただけでテストの内容が把握できるような書き方のほうが、読み手に理解しやすい、想定外の挙動を防ぐことができる。

  • letを使って書くことはこれを実現するために有効

  • letの遅延評価によって、テストによっては必要のないインスタンス変数を毎回beforeで定義するような冗長性を回避できる

が、この遅延評価を理解せずに、なんとなく変数代入の感覚で使用していたために以下のようなエラーに遭遇しました

letを使用することで、通るはずのテストが通らないfalse Alarmを生じる可能性がある

letlet!の使い分けが重要になりそうなケース

  • テストを実施する時点と、前提となる条件の間に時間的なずれがある場合
  • すでに完了しているものと、現在の比較が必要な場合
  • モデルのテストの場合なら、一意性のテスト

遅延評価されるletが、実際にテストの失敗を招いたケース

レビューの内容を参考に、
属性の一意性についてのテストを書いてみた。

User modelの属性usernameの一意性についてのテストを以下のうように書くと

    describe "username" do
      ...
      context "usernameが重複している場合" do
        let(:existing_user) { FactoryBot.create(:user, username: "alice") }
        subject { FactoryBot.build(:user, username: "alice" ) }
        it { is_expected.to be_invalid }
      end
      ...
    end

モデルでusername属性についてのvalidationが記述されているにもかかわらず

class User < ApplicationRecord
  validates :username, presence: true, uniqueness: true, length: { maximum: 12 }
  ...
end

validationがinvalidにならない

1) User#create username usernameが重複している場合 is expected to be invalid
     Failure/Error: it { is_expected.to be_invalid }
       expected `#<User id: nil, email: "test_user_5@example.com", created_at: nil, updated_at: nil, username: "alice">.invalid?` to return true, got false

テストの記述を変更する
(実際にレビューコードではこの書き方を提示してくれていました)
let >> let!

    describe "username" do
      ...
      context "usernameが重複している場合" do
        let!(:existing_user) { FactoryBot.create(:user, username: "alice") } #let!に変更
        subject { FactoryBot.build(:user, username: "alice" ) }
        it { is_expected.to be_invalid }
      end
      ...
    end

このようにすると、想定通りテストがパスするようになる。

letlet!の違い

  • letは、itやexampleが実行されるまで評価されない
  • let!は即座に実行される

it { is_expected.to be_invalid }の時点で、existing_userは存在完了していないといけない
英文でいうところletは現在完了的な振る舞いをして、let!は過去完了の状態をつくってくれると考えると個人的にはしっくりきた。

参考

RSpecのletを使うのはどんなときか?(翻訳) - Qiita

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

letを使用することで、通るはずのテストが通らないFalse Alarmを生じる可能性がある

先日RSpecのテストコードにレビューを頂いたので、

なるべく多くを学びたいと思い、その内容を咀嚼する過程での気付きを書きたいと思います

元記事
RSpecのEmail一意性テストで"Email has already been taken"問題と回避策 - Qiita

  • なるべく小さいscope?(contextの中など)を見ただけでテストの内容が把握できるような書き方のほうが、読み手に理解しやすい、想定外の挙動を防ぐことができる。

  • letを使って書くことはこれを実現するために有効

  • letの遅延評価によって、テストによっては必要のないインスタンス変数を毎回beforeで定義するような冗長性を回避できる

が、この遅延評価を理解せずに、なんとなく変数代入の感覚で使用していたために以下のようなエラーに遭遇しました

letを使用することで、通るはずのテストが通らないfalse Alarmを生じる可能性がある

letlet!の使い分けが重要になりそうなケース

  • テストを実施する時点と、前提となる条件の間に時間的なずれがある場合
  • すでに完了しているものと、現在の比較が必要な場合
  • モデルのテストの場合なら、一意性のテスト

遅延評価されるletが、実際にテストの失敗を招いたケース

レビューの内容を参考に、
属性の一意性についてのテストを書いてみた。

User modelの属性usernameの一意性についてのテストを以下のうように書くと

    describe "username" do
      ...
      context "usernameが重複している場合" do
        let(:existing_user) { FactoryBot.create(:user, username: "alice") }
        subject { FactoryBot.build(:user, username: "alice" ) }
        it { is_expected.to be_invalid }
      end
      ...
    end

モデルでusername属性についてのvalidationが記述されているにもかかわらず

class User < ApplicationRecord
  validates :username, presence: true, uniqueness: true, length: { maximum: 12 }
  ...
end

validationがinvalidにならない

1) User#create username usernameが重複している場合 is expected to be invalid
     Failure/Error: it { is_expected.to be_invalid }
       expected `#<User id: nil, email: "test_user_5@example.com", created_at: nil, updated_at: nil, username: "alice">.invalid?` to return true, got false

テストの記述を変更する
(実際にレビューコードではこの書き方を提示してくれていました)
let >> let!

    describe "username" do
      ...
      context "usernameが重複している場合" do
        let!(:existing_user) { FactoryBot.create(:user, username: "alice") } #let!に変更
        subject { FactoryBot.build(:user, username: "alice" ) }
        it { is_expected.to be_invalid }
      end
      ...
    end

このようにすると、想定通りテストがパスするようになる。

letlet!の違い

  • letは、itやexampleが実行されるまで評価されない
  • let!は即座に実行される

it { is_expected.to be_invalid }の時点で、existing_userは存在完了していないといけない
英文でいうところletは現在完了的な振る舞いをして、let!は過去完了の状態をつくってくれると考えると個人的にはしっくりきた。

参考

RSpecのletを使うのはどんなときか?(翻訳) - Qiita

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue-routerのrouter.goがsafariで機能しない時の対処法

はじめに

vue-routerを使ったSPAアプリケーションを開発している時に、router.goメソッドでリロードを実現していましたが、ChromeからSafariに切り替えて検証したら動作しなかったため、対処法を調べてみました。

this.$router.go

window.location.reload()でブラウザをリロードする

window.location.reload()

ウィンドウ毎リロードすれば、safariでもリロードできます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails から Open Distro for Elasticsearch に接続する

この記事について

Ruby on Rails から Docker 上の Open Distro for Elasticsearch に接続する方法を記載します。

Open Distro for Elasticsearch とは

Open Distro for Elasticsearch は Elasticsearch のディストリビューションで、 Amazon Elasticsearch Service で使われているものです。

Docker

公式のドキュメントを参考に docker-compose で Open Distro for Elasticsearch を立ち上げます。
今回はシングルノードで立ち上げています。

docker-compose.yml
version: "3"
services:
  elasticsearch_open_distro:
    image: amazon/opendistro-for-elasticsearch:1.9.0
    environment:
      - discovery.type=single-node
      - cluster.name=elasticsearch
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ports:
      - 9200:9200

Ruby on Rails から接続

elasticsearch-railselasticsearch-model を使います。

Gemfile
gem 'elasticsearch-model'
gem 'elasticsearch-rails'

素の Elasticsearch と違い Open Distro for Elasticsearch は localhost の場合も https で接続しなければなりません。
よって initializer は以下のように書きます。
localhost の場合は transport_options で ssl の verify を false にしています。
ELASTICSEARCH_USERELASTICSEARCH_PASSWORD はデフォルトでは両方 admin を使うと接続することはできます。

config/initializers/elasticsearch.rb
Elasticsearch::Model.client = Elasticsearch::Client.new(
  host: 'localhost',
  port: 9200,
  user: ENV['ELASTICSEARCH_USER'],
  password: ENV['ELASTICSEARCH_PASSWORD'],
  scheme: 'https',
  transport_options: {
    ssl: {
      verify: false,
    },
  },
)

これで接続することができました。
elasticsearch-rails の Usage の手順で接続を確認することができます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け?】SQL超マスター〜複雑なテーブルにへこたれない編〜【Railsとの比較あり】

はじめに

Railsを利用していると、普通に使っている分にはどんなSQLが発行されるんだろう?ということを考えずとも、欲しいレコードが簡単に取得できますよね。Railsを学び始めた頃からすごいな〜と思っていましたが、今になってもやはり凄いなと思います。

ただ1つ、便利だからこそ生じる弊害があります。それは

>>>SQLが書けなくなる<<<

別に、「SQLが書けなくてもActiveRecordとかが頑張ってくれるから困らないじゃん」と思っている方もいらっしゃるかもしれません。私もそう思っていました。
しかし、例えRailsであっても、テーブル構造が複雑になってくると、どんなSQLを発行したいのか?という部分が分からないと、Railsでどう書いたらデータが取れるのか?というのが分からなくなります。

業務中に、そういった場面と直面する機会が最近格段に増え物凄く困った、危機感を持った、というのが私の体験談であり、結果として「やばい、SQL勉強し直そう」と思うきっかけとなりました。
なんとなくで Rails でレコードを取得している方は、この記事で一緒に学び直しましょう!

環境

  • Docker for Mac 2.3.0.3
  • Ruby 2.6.6
  • Rails 6.0.3
  • MySQL 8.0
  • DBeaver 7.1.0

環境構築

  1. こちらからリポジトリをcloneする
  2. $ docker-compose buildを実行
  3. $ docker-compose up -dを実行
  4. $ docker-compose exec web rails db:create を実行
  5. $ docker-compose exec web rails db:migrateを実行
  6. $ docker-compose exec web rails db:seed を実行
  7. こちらの記事を参考に、MySQLDBeaverを接続する
  8. DBeaver との接続後、students テーブルにレコードが 6 件登録されていることを確認できればOK

※DBeaver との接続時、MySQL の root パスワードを要求されると思います。
パスワードは docker-compose.ymlMYSQL_ROOT_PASSWORD に指定した値を入力してください。(未設定の場合は password が設定されます。)

今回使用するテーブル

スクリーンショット 2020-06-23 14.50.09.png

初級編〜SELECTの基礎を学ぶ〜

※ここからはDBeaverSQLコンソールを使用していきます。sql_master_developmentがデフォルト選択されるよう設定を行って下さい。

universitiesテーブルのレコードを全件取得する

手始めに universitiesテーブルのレコードを全件取得してみましょう。これは簡単ですね!

universitiesテーブルのレコードを全件取得する
 select * from universities;

これは Railsだと以下のようになります。

University.all

特定のUniversityのレコードを取得する

では、次はnameUniversityAのレコードを全件取得しましょう。

nameUniversityAのレコードを全件取得する
 select * from universities where name = 'UniversityA';

これはRailsだと以下のようなイメージです。

University.where(name: 'UniversityA')

whereが出て来たので、find_bySQLで書いてみましょう。
※1件のみ取得したい時はlimitを使用します。

nameUniversityAのレコードを1件取得する
 select * from universities where name = 'UniversityA' limit 1;

中級編 テーブルを結合して、欲しいレコードを取得する

UniversityAに紐づいた studentsレコードを全件取得する

テーブル結合について(簡易版)

universitiesテーブルとstudentsテーブルはuniversity_studentsという中間テーブルによって紐づけられています。

そのため、該当レコードを取得するためにはテーブル同士の結合が必要になります。
テーブル結合には内部結合(JOIN/INNER JOIN)外部結合(LEFT JOIN)があります。

  • 内部結合・・・あるカラムの値が一致しているレコードを取得し、結合して表示する
  • 外部結合・・・あるカラムの値が一致していない場合でもテーブルを結合し、全件表示する

今回のような場合は 内部結合外部結合か考えながら、まずは最終的に欲しい情報であるstudentsテーブルと中間テーブルである university_studentsテーブルを結合して、studentsレコードを全件取得してみましょう。

※結合の構文は inner join(left join) 結合するテーブル名 on 結合先テーブル名.カラム名 = 結合するテーブル名.カラム名

nameUniversityAのレコードを1件取得する
 select students.* from students inner join university_students on students.id = university_students.student_id;

取得できましたか?
これはRailsだと以下のようになります。

Student.joins(:university_student)

結合したテーブルを使って、UniversityAに紐づいた studentsレコードを全件取得する

では、次は本題であるUniversityAに紐付いたstudentsレコードを全件取得していきましょう。
先ほどstudentsテーブルとuniversity_studentsテーブルを結合したので、今回は追加でuniversity_studentsテーブルとuniversitiesテーブルを結合します。

そして、universitiesレコードのnameカラムがUniversityAであるものを特定すると、UniversityAに紐づいた studentsレコードを全件取得することができます。

StudentAStudentFまで取得できていたらOKです :thumbsup:

UniversityAに紐づいた studentsレコードを全件取得する
select students.* from students
inner join university_students on students.id = university_students.student_id 
inner join universities on university_students.university_id = universities.id 
where universities.name = 'UniversityA';

これをRailsで書くと以下のようになります。

Student.joins(:university).where(universities: { name: 'UniversityA' })

この辺りから「Railsスゲー!!」という気持ちが大きくなってくるのではないでしょうか?
joins:universityのみの記述で良いのは、Model(Student)has_one :university, through: :university_studentを定義しているからです。

Railsは関連付けさえ綺麗に定義できれば上記のように記述をどんどん簡略化できるのでよいですね :thumbsup:

UniversityAに所属しているStudentAcourse_registrationsレコードを全件取得する

この辺から関連するテーブルが増えてきてごちゃごちゃします。
一つずつ紐解いて考えていきましょう。

ここでは UniversityA大学に所属しているStudentAさんのcourse_registrationレコード、つまり履修登録情報を取得します。

ここで簡単に仕様を説明します。
course_registrationsレコードは年度毎に作成されます。

StudentAさんは、2020年度の時点で2回生です。
そのため、course_registrationsレコードは2レコード作成されています。

studentsテーブルとcourse_registrationsテーブルはstudent_course_registrationsという中間テーブルを持っています。

UniversityA大学に所属しているStudentAさんのcourse_registrationレコードを全件取得する
select course_registrations.* from course_registrations
inner join student_course_registrations on course_registrations.id = student_course_registrations.course_registration_id
inner join students on student_course_registrations.student_id = students.id
inner join university_students on students.id = university_students.student_id 
inner join universities on university_students.university_id  = universities.id
where universities.name = 'UniversityA'
and students.name = 'StudentA';

これをRailsで書くとこうなります。

 CourseRegistration.joins(student: :university)
                   .where(
                     students: { name: 'StudentA' },
                     universities: { name: 'UniversityA' }
                   )

ここまで書いた私「Railsやばい」

自分で生のSQLを全部書く時間と比較してどうでしょう?Rails凄い。

さて、まだこれはテーブル結合として「まだ」優しいです。
この時点ではまだ、全て年度の履修を登録したという情報しか取得できていません。
最初のER図を見た時に少し嫌な予感がした、という方。正しい判断です。
次は ある年度に履修した全ての科目を取得してみましょう!

UniversityAに所属しているStudentA2020年度のCourseRegistration(履修登録情報)に紐付いたsubjectsレコードを全件取得する

見出しがカオスになってきました。
ただ、ここまでの知識を活かせばそう難しくないと思います。

subjectsテーブルとcourse_registrationsテーブルにも、例によってsubject_course_registrationsという中間テーブルがいます。
では、今までの知識を活かして取得してみましょう!

UniversityAに所属しているStudentA2020年度のCourseRegistration(履修登録情報)に紐付いたsubjectsレコードを全件取得する
select subjects.* from subjects
inner join subject_course_registrations on subjects.id = subject_course_registrations.subject_id 
inner join course_registrations on subject_course_registrations.course_registration_id = course_registrations.id
inner join student_course_registrations on course_registrations.id = student_course_registrations.course_registration_id
inner join students on student_course_registrations.student_id = students.id
inner join university_students on students.id = university_students.student_id 
inner join universities on university_students.university_id  = universities.id
where universities.name = 'UniversityA'
and students.name = 'StudentA'
and course_registrations.year = '2020';

これをRailsで書くと以下のようになります。

Subject.joins(course_registration: { student: :university })
       .where(
         universities: { name: 'UniversityA' },
         students: { name: 'StudentA' },
         course_registrations: { year: '2020' }
       )

やっぱりRailsって凄いですね。。。

UniversityAに所属するTeacherAが受け持っている科目 基礎英語2020年度に受講する生徒を全件取得する

subjectsレコードを取得したから、今度はどうせteachersレコードでも取得するんでしょうと思われた方もいるかもしれません。
ですが、恐らくワンパターンすぎて飽きてきたという方もいるでしょう。
私も流石に(ちょっともういいかな……)と思えてきたため、teachersレコードを追加するのは各自で試してみていただければ、と思います。

今度は生徒ではなく、教員側がデータベースに登録された情報を参照したい場合を考えてみます。

表題のようなレコードが欲しい、というケースですが、例えば 教員が 「今年度の出席簿を作りたいな……」と思った時に、履修対象者を全件取得したい、といった時に起こり得そうですね。

UniversityAに所属するTeacherAが受け持っている科目 基礎英語2020年度に受講する生徒を全件取得する
select distinct students.* from students
inner join student_course_registrations on student_course_registrations.student_id = students.id 
inner join course_registrations on course_registrations.id = student_course_registrations.course_registration_id 
inner join subject_course_registrations on subject_course_registrations.course_registration_id = course_registrations.id
inner join subjects on subjects.id = subject_course_registrations.subject_id
inner join subject_teachers on subject_teachers.subject_id = subjects.id
inner join teachers on teachers.id = subject_teachers.teacher_id
inner join university_teachers on university_teachers.university_id = teachers.id 
inner join universities on university_teachers.university_id = universities.id
where universities.name = 'UniversityA'
and teachers.name = 'TeacherA'
and subjects.name = '基礎英語'
and course_registrations.year = '2020';

これをRailsで書くと以下のようになります。

Student.joins(course_registrations: { subjects: { teachers: :university } })
       .where(
         course_registrations:{
           subjects: { name: '基礎英語' },
           teachers: { name: 'TeacherA' },
           universities: { name: 'UniversityA' },
           course_registrations: { year: '2020' }
         }
       )

上級(?)編 結合したテーブルを使って合計値を出す

各生徒ごとの総取得単位を取得する

ただレコードをとるだけでは面白くないので、次は生徒の名前と、総取得単位を取得し閲覧したいと思います。

取得するのは以下のようなデータです。

student_name total_credit
StudentA 13
StudentB 13
StudentC 8
StudentD 7
・・・ ・・・

今回は 生徒ごとの単位の合計を出すので、studentsテーブルとsubjectsテーブルの情報が必要になります。

1人の生徒には年度ごとに履修登録情報が紐づいていて、履修登録情報各科目の情報が紐づいている、というのは一度SQLを書いたので問題ないと思います。

キモとなるのは、1人ずつ 単位の合計値をまとめたいという点だと思います。
情報をまとめたい場合は GROUP BYを使います。

group by (カラム名)

また、カラムの合計値を出す時は SUM関数を使います。

sum(カラム)

そして、先程の表を見ると、少しヘッダーの表示がカラム名と異なっていたと思います。
ASでエイリアスをつける必要がありそうですね。

上記のことを踏まえて、SQLを書いてみましょう!

各生徒ごとの総取得単位を取得する
select students.name  as student_name, sum(subjects.credit) as sum_credit FROM students
inner join student_course_registrations ON student_course_registrations.student_id = students.id 
inner join course_registrations ON course_registrations.id = student_course_registrations.course_registration_id 
inner join subject_course_registrations ON subject_course_registrations.course_registration_id = course_registrations.id
inner join subjects ON subjects.id = subject_course_registrations.subject_id
group by students.name

これはRailsで書くと以下のようになります。

 Student.joins(course_registrations: :subjects)
        .group('students.name')
        .sum(:credit)

:rolling_eyes:

書く量が全然違いますよね。
ただ、これを書こうと思った時に、結局どんな感じのSQLが発行されて欲しいのか?というところがわからないと、「???」となると思います。(私は毎回そうなっていました)

また、「上みたいな場合だとRubyでなんとかできそうだから、mapとかeach_with_object使ってなんとかしちゃお」と私は思いがちだったのですが、純粋に値が必要なだけであればデータベースから直接取得できるので、Rubyでゴリ押すのではなく、いい感じのSQLを発行して必要な値を取得する、というのも必要なスキルだなと思いました :thumbsup:

最後に

親子関係のあるテーブルならまだ良いのですが、親子孫曽孫……のような構成のテーブルがあった時に、「親から曽孫ってどうやってとるんだ!?!?」と混乱することが多かったのですが、「どのテーブルのレコードが主人公になっているのか?」を意識しながら書いていくとそんなに複雑ではないということが分かったと思います。

また、Railsでレコードを取得する時も少し混乱してしまいがちですが、SQLのテーブル結合を意識するとシンプルに書いていくことができるので、こんなSQLが発行されて欲しい!というのを意識しながら書いていくと良いですね :thumbsup:

Railsでシンプルに書いていこうと思うと、Modelに定義する関連付けが大事だという話を少ししました。テーブルが複雑になればなるほど、この関連付けの定義も難しくなります(ここ最近私が頭を抱えているところです。)

次はRailsで条件付きの has_onehas_manyの定義の仕方を学べるようなQiitaが書けたらと思っています :thinking:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveRecordが接続しているDBの設定情報を確認するコマンド

今回のユースケース

DBクライアントツールにDBの設定情報を入力するために、ActiveRecordが接続しているDBの設定情報を一覧で取得したい。

動作環境

  • Rails 6.0.3.2
  • Microsoft SQL Server 2014

手順

  1. rails cでコンソールを起動する
  2. console内でActiveRecord::Base.connection_configを実行する
$ rails c
Loading development environment (Rails 6.0.3.2)
[1] pry(main)> ActiveRecord::Base.connection_config
=> {:adapter=>"sqlserver",
 :encoding=>"utf8",
 :username=>"hoge",
 :password=>"fuga",
 :tds_version=>7.4,
 :host=>"127.0.0.1",
 :database=>"piyo",
 :timeout=>15000,
 :port=>1433}

※デフォルトのdevelopment環境以外の環境でDBの設定情報を確認したい場合は、rails c -e 環境名で環境を指定してコンソールを立ち上げて、console内でActiveRecord::Base.connection_configを実行することで確認が出来る。

$ rails c -e test
Loading test environment (Rails 6.0.3.2)
[1] pry(main)> ActiveRecord::Base.connection_config
=> {:adapter=>"sqlserver",
 :encoding=>"utf8",
 :username=>"hoge",
 :password=>"fuga",
 :tds_version=>7.4,
 :host=>"127.0.0.1",
 :database=>"piyo_test",
 :timeout=>5000}

参照

https://api.rubyonrails.org/classes/ActiveRecord/ConnectionHandling.html#method-i-connection_config

感想

環境変数で設定している値も一覧で確認できたので便利でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker 初心者がまとめてみた

プログラミング初心者がDocekrについてまとめてみた

どこまでのまとめ?

Dockerについて〜コンテナの作成

コンテナ・イメージについて

Dockerの便利なところ

  • コマンド一発でコンテナを何度でも生成できる

    • 一度Dockerfileを作成してしまえば、簡単に用意できる。
  • コンテナを共有できる

    • Dockerfile(環境構築のレシピ)の中身はソースコードだから他の人たちと共有できる。Dockerfileを共有するためのDockerhubというサービスがあり、他の人が生成したコンテナをいつでも使える。

コンテナとは

コンテナとはDockerによって作成されるゲストOSのことで、 Dockerイメージを元に作成される仮想環境の実行部分。

イメージとは

通常パソコンにOSをインストールする時に使用されるものがイメージと呼ばれる。
アプリケーション開発にはUbuntu、RubyやPHPなど、実行するためにはOSが必ず必要になってくる。 『 OS が必要 = イメージが必要 』となり、結果 Docker の使用にあたって「Dockerイメージ」が必要となってくる。

イメージはコンテナを作成するためのテンプレートとなってくれる。
つまりDockerコンテはDockerイメージから生成される。

ビルドをするとは

DockerfileからDockerコンテナの元となるイメージをつくることを、Dockerイメージをビルドすると呼ばれている。

Dockfileとは

環境構築のレシピのこと。どうやって環境を作成していくかを記したファイル。
DockerfileはベースとなるDockerイメージ(OS)をFROMで定義できる。
Dockerfileはあくまでもイメージを構築するための手順を記述したファイルで、Dockerfile自身がDockerイメージになるわけではない!!!

qiita.rb
FROM ruby:2.6

Dockerfileでイメージをビルドする際、まず最初にFROMで指定されたイメージをDockerHubというレジストリからダウンロードしてから実行される。

Dockerhub

Dockerfile(環境構築のレシピ)の中身はソースコードだから他の人たちと共有できる。Dockerfileを共有するためのDockerhubというサービスがあり、他の人が生成したコンテナをいつでも使える。

ここまでの流れ

Dockerイメージを構築するためのDockerfileを作成し、ビルドをすることによってイメージが作成されて、Dockerコンテナを実行していく。

コマンド/Dockerfileの中身

Dockerfile
FROM ruby:2.6

# install package to docker container
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev \
    && apt-get install apt-transport-https \
    && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update && apt-get install -y yarn \
    && curl -sL https://deb.nodesource.com/setup_10.x | bash - \
    && apt-get install -y nodejs \
    && mkdir /アプリ名

WORKDIR /FANTRA
COPY Gemfile /アプリ名/Gemfile
COPY Gemfile.lock /アプリ名/Gemfile.lock

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

[FROM]

使用するイメージとバージョンを指定。
Dockerfileでイメージをビルドする際、まず最初にFROMで指定されたイメージをダウンロードしてから実行される。

[RUN]

Dockerイメージビルド時に、Dockerコンテナ内で実行するコマンドを定義
ここではrails6に必要なツールをインストール。

[WORKDIR]

作業ディレクトリを設定

[COPY] 

Dockerを動作させているホストマシン上のファイルやディレクトリをDockerコンテナ内にコピーする
左側がローカルで、右側がコンテナ内。

[ENTRYPOINT]

一番最初に実行するコマンド。
ENTRYPOINTはCMDと同じくコンテナ内で実行するプロセスを指定する。

[EXPOSE]

コンテナがlistenするport番号

[CMD]

CMDはコンテナ起動時に1度実行される。RUNでアプリケーションの更新や配置、CMDはアプリケーションそのものを動作させる

Dockerfileやその他必要なファイルができればビルドをする

rails6で開発したかったら、以下のURLを参考にして必要フィルを作成していける。
https://qiita.com/nsy_13/items/9fbc929f173984c30b5d

docker image buildコマンドでDockerイメージを作成

qiita.rb
docker image build -t イメージ名 Dockerfile配置ディレクトリのパス

-tオプションでイメージ名を指定する。
ここでイメージ名を指定しないと、ハッシュ値で管理することになって手間が増えてしまう。
カレントディレクトリがDockerfile配置ディレクトリであれば、最後の引数は「.」(カレントディレクトリ)

docker image buildでは必ずDockerfileを与える必要があるから、ディレクトリにDockerfileが存在しないとちゃんと実行できない。

イメージがbuildで作成されたら

作成したイメージをdocker container runコマンドを利用してコンテナを実行できる。
注意なのがrunコマンドは実行とコンテナを作成するから、作る必要のないコンテナを実行する時は --rmをつけてやる。

コンテナのライフサイクル

Dockerコンテナは実行中・停止・破棄という3つの状態のいずれかに分類される。これをDockerコンテナのライフサイクルと呼びます。docker container runで起動された直後は実行中に当てはまる。

実行中

docker container runで指定されたDockerイメージをもとにコンテナが作成され、DockerfileのCMDやENTRYPOINTで定義されているアプリケーションの実行を開始する。このアプリケーションが実行中なら、Dockerコンテナは実行中にあるということ。

停止

実行中のコンテナはユーザーが明示的にコンテナを停止するか、コンテナで実行されているアプリケーションが正常・異常を問わず終了した場合に自動的に停止。

破棄

頻繁にコンテナの実行・停止を繰り返すような環境ではディスクの要領を専有していくことになるため、不要なコンテナは破棄した方がいい。
コンテナは明示的に破棄しない限り残り続けてしまう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでIPベースでBasic認証をかける

社内システムでかつ、外部公開をする場合に社内なら認証なし+社外からは認証かけたい場合のIPベースでの解決方法

RailsでのBasic認証

https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
まずは公式参照

ほぼこの内容なので、条件をローカルネットワークなら認証必要に変える。
ちなみにこの公式の内容だと通常のログイン認証と併用できたりする。管理者アクセスとかに使えるかも。

application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  http_basic_authenticate_with name: "user", password: "pass"
end

IPベースの判定

CIDRベースで指定したい。
IPAddrnetaddrでいけるみたい。今回はnetaddrの方を使う。

github
https://github.com/dspinhirne/netaddr-rb
readmeに使い方はないので、テストケースを参照。

サブネットで判定

https://github.com/dspinhirne/netaddr-rb/blob/master/test/ipv4net_test.rb
ここにいろいろあるテストケースから抜粋

    def test_contains
        net = NetAddr::IPv4Net.parse("1.0.0.8/29")
        ip1 = NetAddr::IPv4.parse("1.0.0.15")
        ip2 = NetAddr::IPv4.parse("1.0.0.16")
        ip3 = NetAddr::IPv4.parse("1.0.0.7")

        assert_equal(true, net.contains(ip1))
        assert_equal(false, net.contains(ip2))
        assert_equal(false, net.contains(ip3))
    end

このへんですね。
実際に書くと

    def is_local_access?
      return NetAddr::IPv4Net.parse('192.168.1.0/24').contains(NetAddr::IPv4.parse(request.ip))
    end

こんな感じでしょうか。
これで192.168.1.0/24のネットワークが判定できる。
(nginxとかリバースプロキシ挟んでるときはrequest.iprequest.remote_ip、nginxの設定によるかも)

もうちょっとやるならlocalリクエストも判定しておいたほうがいい。
request.local?

application_controller

application_controller.rb
require 'netaddr'

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  http_basic_authenticate_with name: "user", password: "pass", unless: : is_local_access?

  private
    def is_local_access?
      return NetAddr::IPv4Net.parse('192.168.1.0/24').contains(NetAddr::IPv4.parse(request.ip))
    end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nuxt.js × Rails CSVアップロードフォームを作る

はじめに

新卒エンジニアの@yhorikawaです
nuxt.jsとRailsを使ってCSVアップロードフォームを作るのに苦戦したので、記事として残します。

csvファイル

1,hoge,100
2,foo,200
3,bar,300

このようなcsvが存在していて、一行ずつ保存したいと仮定します。

フロント側

<template>
  <div class="container">
    <input @change="handleChange" type="file" name="file">
    <button @click="handleSubmit" type="submit">保存</button>
  </div>
</template>
<script>
export default {
  data: {
    file: null,
  },
  methods: {
    handleChange(e) {
      this.file = e.target.files[0];
    },
    handleSubmit() {
      let formData = new FormData();
      formData.append('file', this.file);
      this.$axios
        .$post('csvFileUploadUrl', formData)
        .then(response => {
          // 成功した時の処理
        }).catch(err => {
         // エラーの時の処理
        });
    },
  },
};
</script>

ファイルをpostできなくて苦戦していたのですが、
FormDataを利用することで簡単にファイルアップロードをすることができました!

Rails側

require 'csv'
def csv_upload
  CSV.foreach(params[:file]) do |row|
    # 保存処理
  end
end

おわりに

今回はファイルアップロードフォームを作成しました。
単純に実装できそうだと思って作り始めたのですが、意外と難しかったので記録として残しておこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでbootstrap-iconsを簡単に使う

rails(webpacker)でbootstrap-iconsを使う方法

現時点ではv1.0.0-alpha5が最新なので今後変わると思います。

npm
npm install bootstrap-icons

yarn
yarn add bootstrap-icons

次に app/helpers/application_helper.rb に以下を追記します。

module ApplicationHelper
  ...
  # 追加
  def icon(icon, options = {})
    file = File.read("node_modules/bootstrap-icons/icons/#{icon}.svg")
    doc = Nokogiri::HTML::DocumentFragment.parse file
    svg = doc.at_css 'svg'
    if options[:class].present?
      svg['class'] += " " + options[:class]
    end
      doc.to_html.html_safe
  end
end

viewはslimを使用しているので

=icon("hdd", class: "text-gray")

で表示されます。

スクリーンショット 2020-07-28 0.10.44.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails devise 導入時のエラー

devise gemを導入時にdb:migrateでエラー発生

以下の手順で導入

Gemfile
gem 'devise'

をGemfileに追加。
その後、gemをインストール

$ bundle install

依存するファイルを作成

$ rails g devise:install

以下の英文が表示される。

===============================================================================

Depending on your application's configuration some manual setup may be required:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

     * Required for all applications. *

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

     * Not required for API-only Applications *

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

     * Not required for API-only Applications *

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

     * Not required *

===============================================================================

上記の内容は今回のエラーとは関係ないので無視で!

ユーザーモデルの作成

$ rails g devise user

マイグレーションファイルが作成されているので、ここで追加したいカラムがあればマイグレーションファイルに記述しておく。

今回の問題点

$ rails db:migrate

マイグレーションファイルをマイグレートしようとすると以下のエラーがでてくる。

Takanori:movie_share_app taka$ rails db:migrate
== 20200727124253 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0137s
== 20200727124253 CreatePosts: migrated (0.0138s) =============================

== 20200727141052 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0182s
-- add_index(:users, :email, {:unique=>true})
rails aborted!
StandardError: An error has occurred, all later migrations canceled:

Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/taka/projects/movie_share_app/db/migrate/20200727141052_devise_create_users.rb:39:in `change'
/Users/taka/projects/movie_share_app/bin/rails:9:in `<top (required)>'
/Users/taka/projects/movie_share_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
ActiveRecord::StatementInvalid: Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/taka/projects/movie_share_app/db/migrate/20200727141052_devise_create_users.rb:39:in `change'
/Users/taka/projects/movie_share_app/bin/rails:9:in `<top (required)>'
/Users/taka/projects/movie_share_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/taka/projects/movie_share_app/db/migrate/20200727141052_devise_create_users.rb:39:in `change'
/Users/taka/projects/movie_share_app/bin/rails:9:in `<top (required)>'
/Users/taka/projects/movie_share_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)

上記のエラーだと、
指定されているキーが長すぎるということを言っているらしいです。
キーの最大長は767バイトということです。
原因としては、DBのcharsetがutf8mb4であったことが原因です。

DBのcharset確認方法

mysql -u root -p

上記でmysqlにログイン

以下の記述でdatabaseの文字コード確認

mysql> show create database [database_nameを記入、 
例: test_development] ; #セミコロン忘れ注意!

以下のようにでてくる。

+-----------------------------+-----------------------------------------------------------------------------------------+
| Database                    | Create Database                                                                         |
+-----------------------------+-----------------------------------------------------------------------------------------+
| test_development            | CREATE DATABASE `test_development` /*!40100 DEFAULT CHARACTER SET utf8mb4 ← **ここがデフォルトのcharset** */ |
+-----------------------------+-----------------------------------------------------------------------------------------+

Rails6以降のverではデフォルトでcharsetはutf8mb4になる。それ以前だと、utf-8がデフォルトだった。

utf8は1文字が3バイト。対して、utf8mb4は1文字が4バイトとなります。
したがって、varchar(255)カラム上では
utf> 255 * 3 = 765byte
utf8mb4> 255 * 4 = 1020byte
こえちゃってますね。

対処方法

1.DBのcharsetをutf-8に変更

以下の記事を参考に!
MySQLの文字コードをutf8mb4に変更

2.varcharの最大長を191にする

デフォルトでvarchar(255)となっているものを、varchar(191)とする。
変更方法は、migrationファイルで行います。
migrateする前に各カラムにlimit: 191を追記する

*_devise_create_users.rb
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: "", limit: 191 #limitを記述
      t.string :encrypted_password, null: false, default: "", limit: 191 #limitを記述

      ## Recoverable
      t.string   :reset_password_token, limit: 191 #limitを記述
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

もう一つ方法があるのですが今回は割愛します。

これでrails db:migrateができるようになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

renderメソッドとredirect_toの違い

※この記事は私の勘違いが含まれています。正確な内容はコメント欄でご指摘頂いている通りです。

 ツイッターのようなツイートアプリを作成中なのですが、表題の通りrenderとredirect_toの違いってなんや?
と思い私なりの結論が出たのでアウトプットのため、記事にしています。

結論

  • renderは同じコントローラー内のファイルしか基本読み込まないため、同じコントローラーのビューファイルを表示されることに向いている。
  • redirect_toはurlを指定できるので、別のコントローラーのindexのビューファイルを表示させたい場合に向いている。

この結論に至った経緯は以下の通りです。
まずは以下のコードを見てください。

app/controllers/users_controller.rb
class UsersController < ApplicationController

  def edit
  end

  def update
    if current_user.update(user_params)
      redirect_to root_path
    else
      render :edit
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email)
  end
end

これはユーザー管理用のusersコントローラーの記述です。
updeteアクションで、ユーザーアカウントの編集ができた場合はroot_path(messageコントローラーのindexアクション)へリダイレクトする。
できなかった場合はrender :editで再度編集画面(usersコントローラーのeditのビューファイル)へ戻すというものです。

ではredirectrenderに変えた場合どうなるのか考えてみました。

def update
    if current_user.update(user_params)
      render template : "messages/index"
    else
      render :edit

これで同じになったやろ!
と思ったらエラーになりました。
原因はmessage/indexのビューファイルは部分テンプレートを使用していたからでした。

app/views/messages/index.html.erb
<div class="wrapper">
  <div class="side-ber">
    <%= render "side_bar" %>
  </div>
  <div class="chat">
    <%= render "main_chat" %>
  </div>
</div>

 ここでmessagesフォルダ内にある部分テンプレートside_bar,main_chatを読み込むという記述があります。
 試しに部分テンプレートをapp/views/usersフォルダの直下に入れたら正常に表示されました。

以上のことからわかったこと

  • usersコントローラーからmessages/indexのビューファイルだけを呼び出した。
  • ビューファイル内に部分テンプレートを呼び出すコードがあったためusersのビューファイル内で部分テンプレートを呼び出そうとしたが部分テンプレートがないためエラーになった。

まとめ

  • 同じコントローラーのビューファイルを指定する場合はrenderの方が早い
  • redirect_toはリクエストがルーティングに戻されるので別コントローラーのビューファイルを表示させる時に使う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む