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 非エンジニアよ、エンジニアになれ
2020/12/30 decorator
参考
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
のオプションをコントローラに追加することで他のスタイルも使える。