チワワかわいいブログ

RUNTEQでrails勉強する日々の記録

Ruby silver勉強

途中まで書いたのが消えた
Ruby silverの勉強ノート

文法

ブロックを受けるメソッド
  • 配列のeachメソッド 要素に対してブロック内の処理が順に実行される
[1, 2, 3].each do |value|
  p value
end  #=> 1, 2, 3が出力
  • each_with_indexメソッド 引数を2つ取り、第2引数に配列のインデックス(配列の中で何番目か)が指定される
[3, 4, 5].each_with_index do |value, index|
  puts "index => #{index} 数字 => #{value}"  
end
#=>
index => 0 数字 => 3
index => 1 数字 => 4
index => 2 数字 => 5
=> [3, 4, 5]
  • ハッシュのeachメソッド キーと値をそれぞれ第1引数、第2引数に受ける
{a:1, b:2}.each do |key, value|
  puts "#{key}:#{value}"
end
#=>
a:1
b:2
=> {:a=>1, :b=>2}
  • each_keyメソッド、each_valueメソッド キー、値のみを受ける
{a:1, b:2}.each_key do |key|
  puts "key => #{key}"
end
#=>
key => a
key => b

{a:1, b:2}.each_key do |value|
  puts "value => #{value}"
end
#=>
value => 1
value => 2
脱出構文
  • next
10.times do |i|
   next if i == 5
   print i, " "
end  
#=>
0 1 2 3 4 6 7 8 9 => 10
10.times do |i|
   redo if i == 5
   print i, " "
end
#=>
0 1 2 3 4 .... ループ

2021/01/09 ブックマークボタンのajax化

ajaxとは

「Asynchronous JavaScript + XML」の略
Asynchronous →非同期の
JavaScript + XMLを使って非同期の通信を行うこと
通常はリクエストを送信すると、全ての情報を通信してレスポンスが戻ってくる。
ajaxの通信ではリクエストにたいしてレスポンスが戻ってくる(サーバーが処理している)間にも他の作業を行うことができる。
また、ページの一部のみリクエストして更新することができる。
railsにおいてはリクエストのなかで「remote: true」にすることで実装できる。

remoteオプション
<%= link_to 'tests#indexへリンク', ○○(tests#indexへのパス), local: true %>

とすると、対応するviewであるindex.html.erbに遷移する

<%= link_to 'tests#indexへリンク', ○○(tests#indexへのパス), remote: true %>

とすると、レンダリング先がjsファイルになる。
indexアクションを通過した後、index.html.erbではなく、index.js.erbファイルに向かうことになる。 そして拡張子が「js.erb」となっているこのファイル内にはjsの処理+rubyの記述を用いることができる。

js.erbファイルの作成

create、destroyアクションに対するテンプレートをそれぞれ作成

#create.js.erb
document.getElementById("js-bookmark-button-for-board-#{board.id}").innerHTML = '<%= j(render 'unbookmark', board: board) %>'

#destroy.js.erb
document.getElementById("js-bookmark-button-for-board-#{board.id}").innerHTML = '<%= j(render 'bookmark', board: board) %>'

このCSS idのcreateアクションが実行されたときに、unbookmarkのテンプレートをレンダリングする
CSS idはbookmarkのパーシャルからコピペしてきたけどうまく動かない・・ undefined local variable or method `board' というエラーになるので、テンプレートを見直してみる
まず一つ目の#{board.id}は埋め込みrubyでの記載になるので<%= board.id %>に変更
これでもうまく動かず同じエラー。 テンプレートでローカル変数を使ってるので動かないのでは?と思い、コントローラ側をインスタント変数に変更し、テンプレートも変更

class BookmarksController < ApplicationController
  def create
    @board = Board.find(params[:board_id])
    current_user.bookmark(@board)
  end

  def destroy
    @board = current_user.bookmarks.find(params[:id]).board
    current_user.unbookmark(@board)
  end
end
document.getElementById("js-bookmark-button-for-board-<%= @board.id %>").innerHTML = '<%= j(render 'unbookmark', board: @board) %>'

元々のbookmarkパーシャルはパーシャル元から渡すときにlocal変数にしているが、js.erbで参照するのはアクションの変数のためインスタンス変数に変更が必要と理解
これでうまくいった!わけ分からなかった・・ と思いきや今度はCSS idが重複してしまうエラー

#create.js.erb
document.getElementById("js-bookmark-button-for-board-#{board.id}").innerHTML = '<%= j(render 'unbookmark', board: board) %>'

これだと指定したidのHTMLの中身を入れ替えることになり、その入れ替え先にも同じidがあるのでidが重複することになってしまう
調べてみるとjqueryというやつで簡単にできそうなので書き換え

$("#js-bookmark-button-for-board-<%= @board.id %>").replaceWith('<%= j(render 'unbookmark', board: @board) %>');

これで動くようになった
jqueryとか全然わかってないけど、とりあえずどんな役割で何をするためのものなのかがなんとなく理解できたのでよしとする

2021/01/07 掲示板のbookmarkの追加

やること

掲示板をブックマーク/解除出来る機能の追加
ブックマークの一覧表示の機能を追加

考えること

一つの掲示板に対し、ブックマークは複数
一つのユーザーに対し、ブックマークは複数
一つのブックマークに対し、掲示板はひとつ、ユーザーはひとつ
bookmarkテーブルには掲示板とユーザーIDのカラムを持たせ、bookmarkのcreateアクションでそれを 保存し、掲示板の中でbookmarkテーブルにその掲示板のidがあるかどうかでだしわけできるようにすれば良い?
bookmarkのカラムは以下の通り作成。

class CreateBookmarks < ActiveRecord::Migration[5.2]
  def change
    create_table :bookmarks do |t|
      t.references :board_id, foreign_key: true
      t.references :user_id, foreign_key: true

      t.timestamps
    end
    add_index :bookmarks, :board_id
    add_index :bookmarks, :user_id
    add_index :bookmarks, [:board_id, :user_id], unique: true
  end
end

board/userモデルにhas_many、bookmarkモデルにbelongs_toをそれぞれ記述。
bookmarksコントローラを作成し、パーシャルをbookmark_area.html.erb(showテンプレートに記述する用)、 bookmark.html.erb(ブックマーク済ボタンを表示する用)、unbookmark.html.erb(未ブックマークを表示する用)で3つ作成。
やることは、

  • 現在のユーザーがそのboardに紐づくbookmarkを持っているか判断するメソッドを作り、ボタンのだしわけができるようにする
  • フォームからboard_id、user_idを取得し、bookmarkのcreateアクションでbookmarkモデルに保存(このフォームはbookmark.html.erbに記述)
  • 削除用のdestroyアクションも作る(フォームはunbookmark.html.erbに作成)
  • ブックマークボタンを表示したい掲示板一覧に、bookmark_area用のパーシャルを追加、この中で、bookmark有無によって表示をだし分けできるようにメソッドを作っていく
  • 現在のユーザーのブックマーク一覧を取得し、表示するページを作る
    やったこと

    ルーティングの追加

resources :boards, only: %i[index new create show edit destroy update] do
    resources :comments, shallow: true
    resources :bookmarks, shallow: true
    collection do
      get :bookmarks
    end
end

アソシエーションの追加

#user.rb
has_many :bookmarks, dependent: :destroy

#board.rb
has_many :bookmarks, dependent: :destroy

#bookmark.rb
belongs_to :board
belongs_to :user

切り分け用のメソッド

#board.rb
def bookmarked?(user)
    bookmarks.where(user_id: user.id).exists?
end

ブックマークボタン作成

<% unless current_user.own?(board) %> #掲示板が現在のユーザーのものでなければブックマークボタンを表示
  <% if board.bookmarked?(current_user) %> #現在のユーザーがブックマークしている掲示板かを判定
    <%= link_to board_bookmarks_path(board),method: :post, local: true, id: "js-bookmark-button-for-board-#{board.id}" do %> #ネストしている
      <i class="far fa-star"></i>
    <% end %>
  <% else %>
    <%= link_to bookmark_path(board),method: :delete, local: true, id: "js-bookmark-button-for-board-#{board.id}" do %> #shallowオプション
      <i class="fas fa-star"></i>
    <% end %>
  <% end %>
<% end %>

create、destoryアクションを作成

class BookmarksController < ApplicationController
  def create
    board = Board.find(params[:board_id])
    board.bookmarks.create!(user_id: current_user.id)
    redirect_to boards_path, success: t('.success')
  end

  def destroy
    bookmark = Bookmark.find_by(board_id: params[:id], user_id: current_user.id)
    bookmark.destroy!
    redirect_to boards_path, success: t('.success')
  end
end

これで一通り動くようになった!回答例見るとあまりいい出来ではないようなので、解説見ながらリファクタリング

改修

boardとuserを関連付ける。

#user.rb
has_many :bookmarks, dependent: :destroy
has_many :bookmark_boards, through: :bookmarks, source: :board

railsチュートリアルで出てきたけど全然理解できていなかったhas_manyのthroughオプションを使う
userモデル、boardモデルそれぞれに関連づけられているbookmarkモデルを通じてuserとboardを関連づける
この関連付けを行うことでbookmarkにあるboardの情報を、bookmarkを介してuserから取得できるようになる
具体的には

user_object.bookmark_boards.all

のように、第一引数をメソッド名として、userがブックマークしている掲示板の情報を取得するというようなメソッドが使えるようになる
続く引数でなんの情報が欲しいかを指定しているため、第一引数のメソッド名はなんでもいいが他と名前が被らないように指定する必要がある
これを使って、userがブックマークしている掲示板一覧を出すメソッドを書き換える

#board.rb
def bookmarked?(board)
    bookmark_boards.include?(board) #メソッドの引数に指定したboardがレシーバーのuserがブックマークしている中に含まれているか?
end

userオブジェクトのインスタンスメソッドになるため、user.rbに変更
これに応じてviewも書き換え

<% if current_user.bookmarked?(board) %>

また、create、editアクションもthroughオプションで使えるようになったメソッドを使って書き換える
コントローラの可読性をよくするために、メソッドはuser.rbに記載

#user.rb
def bookmark(board)
   bookmark_boards << board
end

def unbookmark(board)
   bookmark_boards.delete(board)
end

collection<<メソッドは、1つ以上のオブジェクトをコレクションに追加します。このとき、追加されるオブジェクトの外部キーは、呼び出し側モデルの主キーに設定されます。

user_object.bookmark(board) で、user_idを外部キーとしてboardがbookmarkに追加される

# 中間テーブルを削除する
bookmarks.destroy(bookmarks)

# お気に入りの掲示板の集合から対象の掲示板を削除する
bookmark_boards.destroy(board)

destroyアクションは中間テーブル(bookmark)を削除するか、userがブックマークしている掲示板をbookmarkから削除するか、どちらでも挙動は同じ

#bookmarks_controller.rb
class BookmarksController < ApplicationController
  def create
    board = Board.find(params[:board_id])
    current_user.bookmark(board)
    redirect_back fallback_location: root_path, success: t('.success')
  end

  def destroy
    board = current_user.bookmarks.find(params[:id]).board
    current_user.unbookmark(board)
    redirect_back fallback_location: root_path, success: t('.success')
  end
end

redirect_back fallback_location:で、元のページにリダイレクト、例外ならrootへという指定ができる(アクションが例外を出した時用?)

2021/01/04 shallowオプションとか

ネスとしたルーティングに対してのform_with
resources :boards, only: %i[index new create show] do
    resources :comments, shallow: true
end

としてルーティングを作成。
form_withでcommentに対してpostリクエストを送りたい。

def show
    @board = Board.find(params[:id])
    @comment = Comment.new
end

フォーム用に@comment変数を作成

<%= form_with model: [@board, @comment], local: true do |f| %>
      <%= f.label :body %>
      <%= f.text_area :body, class: "form-control mb-3", rows: 4 %>
      <%= f.submit t('.submit'), class: "btn btn-primary" %>
<% end %>
モデルの引数に [ 親 , 子 ]として引数を渡す

<form action="/boards/28/comments" accept-charset="UTF-8" method="post">
ルーティング通りネストされたリクエストが作成される
<%= form_with model:[board, comment], local: true do |f| %>

<%= form_with model: comment, url: [board, comment], local: true do |f| %>

前者のほうが記述がコンパクトに書けますが、後者のほうが作成更新の対象となるモデルをmodelに単一で指定しているので明確になる

モデルにインスタンスメソッドを記載する
   # Userモデルに記載した場合、selfレシーバからcurrent_userを取得できるため、引数にcommentを渡してuserを取得して判定 
   def my_comment?(comment)
     self == comment.user
   end
   # if current_user.my_comment?(@comment) という形式でViewで記載
 → if current_user  == comment.user となる

モデルにメソッドを定義するとそのモデルの中でそのメソッドが使える

# クラスメソッドとして定義すると、実行時に判定用のオブジェクトすべてを引数として渡す必要がある。
  # if User.my_comment?(current_user, @comment)

  # インスタンスメソッドとして定義すれば、判定メソッド実行時の引数に渡すオブジェクトが少なくて済む。
  # if current_user.my_comment?(@comment)

クラスメソッドとインスタンスメソッドの対象
クラスは属性やメソッドを定義しているものなので、属性を持っているわけではない
例えばUser.emailのようなインスタンスメソッドをクラスに対して使うことはできない
上で記述している

comment.user

は、アソシエーションに酔って使えるようになったメソッド。
commentの属性user_idを使ってUserテーブルからレコードを検索する=SQLが発行される

comment.user_id

とすると、オブジェクトのuser_idを持ってくるだけ=SQLが発行されないので、負荷が減る

リファクタリング
  • createアクション
  def create
    @comment = current_user.comments.build(comment_params)
    @comment.board_id = params[:board_id]
    @board = Board.find(params[:board_id])
    if @comment.save
      flash[:success] = t('.success')
      redirect_to board_path(params[:board_id])
    else
      flash.now[:danger] = t('.fail')
      render 'boards/show'
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:body)
  end
end

createアクションはparamsから受け取った値をオブジェクトに渡して保存するだけなので、インスタンス変数として保持する必要がない。
不要なインスタンス変数は使わないように注意

class CommentsController < ApplicationController
  def create
    comment = current_user.comments.build(comment_params)
    if comment.save
      redirect_to board_path(comment.board), success: t('.success')
    else
      redirect_to board_path(comment.board), danger: t('.fail')
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:body).merge(board_id: params[:board_id])
  end
end

.merge(board_id: params[:board_id])
mergeメソッドで受け取ったparamsと別のテーブルの情報をあらかじめ一緒に保存することができる

  • 部分テンプレートではローカル変数を使用する
<%= render @comments, { comments: @comments } %>

のようにオプションで変更できる

2020/12/31 carrier_wave

掲示板に画像投稿機能をつける carrier_waveとmini_magickという2つのgemを使う
参考 techtechmedia.com carrier_wave→ファイルアップロード用のgem
mini_magick→ファイル編集用のgem。利用にあたりImageMagickというソフトのインストールが必要。

実装する
$ brew install imagemagick #ImageMagickをインストール

/gemfile
gem 'carrierwave'
gem 'mini_magick'

$ bundle install

ここまで実行すると、アップロード用のクラスを作成するコマンドを実行できるようになる。
画像のフォーマットやサイズなどの設定をこのクラスに記述することになる。
モデルが異なる場合でも記述した設定を使いまわしたりできるので、色々なモデルでアップローダーを使う場合に柔軟に対応できる。

$ rails generate uploader image

次にboardモデルに新しいカラムを作成。

class AddBoardImageToBoards < ActiveRecord::Migration[5.2]
  def change
    add_column :boards, :board_image, :string
  end
end

このカラムに対して画像をアップロードして、それをアップローダーが処理することになるため、
カラムとアップローダーの関連付けを行う。

class Board < ApplicationRecord
  mount_uploader :board_image, ImageUploader
end

アップローダーにminimagick経由で加工するため追記。

#image_uploader.rb

class ImageUploader < CarrierWave::Uploader::Base 
  include CarrierWave::MiniMagick
  process resize_to_fit: [300, 200]
end

画像投稿のviewを編集して、画像用のフォームを追加 。

<%= f.label :board_image, t('.thumbnail') %>
<%= f.file_field :board_image, class: 'form-control' %>

画像の拡張子を制限

#image_uploader.rb

def extension_whitelist
    %w(jpg jpeg gif png)
end

javascriptを使って洗濯した画像のサムネイルを表示

<div>
  <% if @board.board_image.present? %>
  <%= image_tag @board.board_image, id: :img_prev %>
 <% else %>
  <%= image_tag '70017892-c40bbc00-15c7-11ea-8022-9cb7e5d22aa1.png', id: :img_prev %>
 <% end %>
  <script type="text/javascript">
    $(function() {
      function readURL(input) {
          if (input.files && input.files[0]) {
          var reader = new FileReader();
          reader.onload = function (e) {
      $('#img_prev').attr('src', e.target.result);
          }
          reader.readAsDataURL(input.files[0]);
          }
      }
      $("#board_board_image").change(function(){
          readURL(this);
      });
    });
  </script>

フォームで保存した画像は、image_tag 〇〇.urlで表示できる

<%= image_tag board.board_image.url  %>
ポイントを見ながらリファクタリング
  • avascriptの記載はapplication.jsに記載せず、別途専用ファイルを定義する

  • 画像アップロードがない場合のデフォルト画像を、image_uploader.rbに設定しておく
    →indexやshowファイルにif文を描かなくても、勝手にデフォルト画像が表示されてくれる

  • ローカル環境でアップロードした画像ファイルはアップロードしないようgitignoreを追記していること

    • すでにファイルをコミットしてからgitignoreを追記してもファイルが管理対象に含まれているため、コミット後はgit rm --cached ファイル名コマンドでgitの管理対象外に設定する
    • 不要なファイルをpushしてしまった場合は、git rm ファイル名コマンドでリモートリポジトリから削除する
  • board_image_cacheの補足説明
    • アップロードに失敗した際もファイルが消えないようにするために必要 →他のフォームの不備で保存できなかった場合でも保持してくれる

参考

Rails:CarrierWaveで画像のプレビュー機能を実装 | Boys Be Engineer 非エンジニアよ、エンジニアになれ

https://workabroad.jp/tech/1118

2020/12/30 decorator

参考

qiita.com

decoratorとは

ソフトウェアのデザインパターンの一つです。
既存のオブジェクトを新しいDecoratorオブジェクトでラップすることで既存の関数やクラスの中身を直接触ることなく、その外側から機能を追加したり書き換えたりする。
また、既存のクラスを拡張する際にクラスの継承の代替手段として用いられます。

基本的にはビューに表示するためのロジックを記述する場所。

helperやモデルに記述しない理由

それぞれの役割分担をきっちりすることでコードをわかりやすく書く必要がある。
モデル→DBにアクセスする処理を書く
helper→モデルから独立しており、モデルに直接関係していない処理を書く
decorator→特定のモデルに関連した処理を書く

導入する
gem 'draper'
#bundle installする

rails generate draper:install
#アプリケーションにデコレーター層(デコレータ用のフォルダ)を作る  

rails generate decorator User
#モデルに紐づいたデコレータファイルを作成する

delegate_allとは、UserモデルのメソッドをUserDecoratorクラスでも使用できるようにするための記述です。これがあることによってuser.nameなどuserモデルが所有するインスタンスメソッドがUserDecoratorクラスのオブジェクトにも使用することが可能となります。

メソッドを定義する
class UserDecorator < Draper::Decorator
  delegate_all

  def full_name
    last_name + ' ' + first_name
  end
end

viewから呼び出すときは

<%= current_user.decorate.full_name %>

のようにdecorate.を記述する

2020/12/30 flash

やったこと

登録成功・失敗時にフラッシュメッセージを出す

今まで

if @user.save
   flash[:success] = 'ユーザー登録が完了しました'
   redirect_to login_path

と記述していたが実は

if @user.save
   redirect_to login_path,    flash: { success:  'ユーザー登録が完了しました'}

のようにredirect_toのオプションとして記述することもできる デフォルトではflashのスタイルはalert/noticeのみだが、

add_flash_types :success, :info, :warning, :danger

のオプションをコントローラに追加することで他のスタイルも使える。