- 投稿日:2020-06-25T22:33:10+09:00
【初学者手順書2】Ruby on Rails :Railsの雛形作成
初学者手順書1の続きです。
今回は、Railsの雛形を作成していきます。Rails雛形作成
Railsアプリケーションを作成
ターミナル上、指定のディレクトリで実行% rails _Ver.指定_ new アプリ名 -d RDBMS名(mysqlなど)ファイルの文字コードの設定(必要な場合)
config/database.yml# m4を削除 encoding: utf8データベース作成
% rails db:create不要なファイルが作成されないように追記(必要な場合)
rails gでコントローラを作成する場合テストファイルなどを自動で作るためconfig/application.rb# 省略 module アプリ名 class Application < Rails::Application config.load_defaults 6.0 config.generators do |g| g.stylesheets false g.javascripts false g.helper false g.test_framework false end end endGitHubコミットされたくないファイルを記載
.gitignorepublic/uploads/*READMEに、データベース設計やアプリの情報を記載していく。
- 投稿日:2020-06-25T21:42:05+09:00
Rails Tutorial 第6版 学習まとめ 第10章
概要
この記事は私の知識をより確実なものにするためにRailsチュートリアル解説記事を書くことで理解を深め
勉強の一環としています。稀にとんでもない内容や間違えた内容が書いてあるかもしれませんので
ご了承ください。
できればそれとなく教えてくれますと幸いです・・・この章でやること
ユーザーの作成、ログイン、ログイン情報の記憶が実装できたので
次はユーザーリソースで放置していた更新、表示、削除機能を作成する。ユーザーを更新する
ユーザーを更新するにはeditアクションを編集する。
今迄に実装してきたsessionsコントローラのnewアクションや
Usersコントローラのnewアクションのようにフォームを用意し、
フォームの入力値をupdateアクションに送るという動作を実装すればいい。
もちろん編集が可能なのはユーザー本人だがこれまでに実装した認証を使って
アクセス制御を実装していく。編集フォーム
editページはURLに対象のユーザーのIDが含まれる
ex) users/1/editこれを利用してURLのIDからユーザーを取り出しインスタンス変数に保存しておく。
def edit @user = User.find(id: params[:id]) endこうすることで次に作成するフォームでモデルオブジェクトに
@user
オブジェクトを指定する。edit.html.erb<% provide(:title, "Edit user") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(model: @user, local: true) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Save changes", class: "btn btn-primary" %> <% end %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="https://gravatar.com/emails" target="_blank">change</a> </div> </div> </div>フォームに無効な値を入力した際のエラーメッセージを表示するために_error_messagesパーシャルを再利用している。
またgravatarのリンク部に
target="_blank"
とあるが、このように記述することで新しいタブでリンク先を表示できる。さらにフォームの入力欄には@user変数に現在入っている値が自動入力される。
Rails側で自動的に保存されている属性情報を引っ張ってきて表示してくれるらしい。ぐう有能。このerbから生成される実際のHTMLをのぞくと
<input type="hidden" name="_method" value="patch">
このような記述がある。Webブラウザは更新のリクエストであるPATCHリクエストを送信できないためRailsが
隠しinputフィールドにpatchを指定して、疑似的にPATCHリクエストとして偽造している。もう一つ覚えておきたい事項としてnewアクションとeditアクションでほぼ変わらないerbコードを使っているのに
なぜRailsが新規のユーザーか既存のユーザーか判別できるかだが
ActiveRecordのnew_record?
メソッドで新規か既存か判別できるからである。>> new_user = User.new (1.3ms) SELECT sqlite_version(*) => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil> >> user1 = User.first User Load (0.6ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "take", email: "take.webengineer@gmail.com", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-20 03:53:57", password_digest: [FILTERED], remember_digest: "$2a$12$tYO.HIfYezXpTk2zRp9s6uqJY4wUkPM28NfYuJ7vxq/..."> >> new_user.new_record? => true >> user1.new_record? => false >>実際のerbでは
form_withを使った際モデルオブジェクトに対して
new_record?```の結果を見て
postかpatchかを判定する。最後にナビゲーションバーのeditアクションへのリンクを設定する。
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
演習
1.興味がわいたので具体的にどのような脆弱性があるか調べた結果、以下の記事が参考になったので置いておく。
https://webegins.com/target-blank/
"noopener"超重要!2.newビューとeditビューではフォームの部分がほぼ共通で、違う点といったら
submitボタンのテキストぐらいである。
そのため、provideメソッドを使ってsubmitボタンのテキストコンテンツを変更するようにし、
パーシャルにまとめてリファクタリングする。_form.html.erb<%= form_with(model: @user, local: true) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit yield(:btn_text), class: "btn btn-primary" %> <% end %>new.html.erb<% provide(:title, 'Sign up') %> <% provide(:btn_text, "Create my account") %> <h1>Sign up</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> </div> </div>edit.html.erb<% provide(:title, "Edit user") %> <% provide(:btn_text, "Save changes") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="https://gravatar.com/emails" target="_blank" rel="noopener">change</a> </div> </div> </div>編集の失敗
ユーザー登録と同じく、無効な値で更新しようとした際の編集の失敗に関して実装していく。
updateアクションを実装していくがcreateアクションでparamsを使ってユーザーを作成したのと同じく
editアクションから送信されたparamsを使って更新する。
構造はかなり似ている。
もちろんparamsでDBを直接更新するのは危険なため今回もStrongParameter(以前定義したuser_paramsメソッド)を使う。def update @user = User.find(params[:id]) if @user.update(user_params) else render 'edit' end end現時点でUserモデルのバリデーションと_error_messagesパーシャルが存在するため
無効な値にはエラーを返してくれるようになっている。演習
編集失敗時のテスト
ユーザーの編集関連の統合テストを作成する。
今回は題目通り編集失敗時のテストを書いていく。
rails g integration_test user_edit
users_edit_test.rbrequire 'test_helper' class UserEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do get edit_user_path(@user) assert_template 'users/edit' patch user_path(@user) , params:{user:{name: "", email: "foo@bar", password: "foo", password_confirmation: "bar"}} assert_template 'users/edit' end end1.editページにGETリクエスト、editページが描画されているか確認
2.updateアクションにpatchリクエスト、無効な値を送信
3.editページが再描画されているか確認。の順のテストになる。
演習
1.
assert_select 'div.alert', "The form contains 4 errors."
TDDで編集を成功させる
今度は成功時の動作を実装していく。
ユーザ⁻画像はGravatarで実装しているためすでに動作する。
name,email,passwordなどほかの属性の編集の成功を実装していく。機能を実装する前に統合テストを書き、機能を実装し終わったときその機能が受け入れ可能な状態かどうか決めるテスト
を「受け入れテスト」と呼ぶ。
実際にTDDで編集成功を実装してみる。先程実装した失敗時のテストを参考に実装していくとわかりやすい。(もちろん有効なデータを送信するが)
test "successful edit" do get edit_user_path(@user) assert_template 'users/edit' name = "foo" email = "foo@bar.com" patch user_path(@user) , params:{user:{name: name, email: email, password: "", password_confirmation: ""}} assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email endり確実なものにするためにRailsチュートリアル解説記事を書くことで理解を深め
勉強の一環としています。稀にとんでもない内容や間違えた内容が書いてあるかもしれませんので
ご了承ください。
できればそれとなく教えてくれますと幸いです・・・この章でやること
ユーザーの作成、ログイン、ログイン情報の記憶が実装できたので
次はユーザーリソースで放置していた更新、表示、削除機能を作成する。ユーザーを更新する
ユーザーを更新するにはeditアクションを編集する。
今迄に実装してきたsessionsコントローラのnewアクションや
Usersコントローラのnewアクションのようにフォームを用意し、
フォームの入力値をupdateアクションに送るという動作を実装すればいい。
もちろん編集が可能なのはユーザー本人だがこれまでに実装した認証を使って
アクセス制御を実装していく。編集フォーム
editページはURLに対象のユーザーのIDが含まれる
ex) users/1/editこれを利用してURLのIDからユーザーを取り出しインスタンス変数に保存しておく。
def edit @user = User.find(id: params[:id]) endこうすることで次に作成するフォームでモデルオブジェクトに
@user
オブジェクトを指定する。edit.html.erb<% provide(:title, "Edit user") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(model: @user, local: true) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Save changes", class: "btn btn-primary" %> <% end %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="https://gravatar.com/emails" target="_blank">change</a> </div> </div> </div>フォームに無効な値を入力した際のエラーメッセージを表示するために_error_messagesパーシャルを再利用している。
またgravatarのリンク部に
target="_blank"
とあるが、このように記述することで新しいタブでリンク先を表示できる。さらにフォームの入力欄には@user変数に現在入っている値が自動入力される。
Rails側で自動的に保存されている属性情報を引っ張ってきて表示してくれるらしい。ぐう有能。このerbから生成される実際のHTMLをのぞくと
<input type="hidden" name="_method" value="patch">
このような記述がある。Webブラウザは更新のリクエストであるPATCHリクエストを送信できないためRailsが
隠しinputフィールドにpatchを指定して、疑似的にPATCHリクエストとして偽造している。もう一つ覚えておきたい事項としてnewアクションとeditアクションでほぼ変わらないerbコードを使っているのに
なぜRailsが新規のユーザーか既存のユーザーか判別できるかだが
ActiveRecordのnew_record?
メソッドで新規か既存か判別できるからである。>> new_user = User.new (1.3ms) SELECT sqlite_version(*) => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil> >> user1 = User.first User Load (0.6ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "take", email: "take.webengineer@gmail.com", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-20 03:53:57", password_digest: [FILTERED], remember_digest: "$2a$12$tYO.HIfYezXpTk2zRp9s6uqJY4wUkPM28NfYuJ7vxq/..."> >> new_user.new_record? => true >> user1.new_record? => false >>実際のerbでは
form_withを使った際モデルオブジェクトに対して
new_record?```の結果を見て
postかpatchかを判定する。最後にナビゲーションバーのeditアクションへのリンクを設定する。
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
演習
1.興味がわいたので具体的にどのような脆弱性があるか調べた結果、以下の記事が参考になったので置いておく。
https://webegins.com/target-blank/
"noopener"超重要!2.newビューとeditビューではフォームの部分がほぼ共通で、違う点といったら
submitボタンのテキストぐらいである。
そのため、provideメソッドを使ってsubmitボタンのテキストコンテンツを変更するようにし、
パーシャルにまとめてリファクタリングする。_form.html.erb<%= form_with(model: @user, local: true) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit yield(:btn_text), class: "btn btn-primary" %> <% end %>new.html.erb<% provide(:title, 'Sign up') %> <% provide(:btn_text, "Create my account") %> <h1>Sign up</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> </div> </div>edit.html.erb<% provide(:title, "Edit user") %> <% provide(:btn_text, "Save changes") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="https://gravatar.com/emails" target="_blank" rel="noopener">change</a> </div> </div> </div>編集の失敗
ユーザー登録と同じく、無効な値で更新しようとした際の編集の失敗に関して実装していく。
updateアクションを実装していくがcreateアクションでparamsを使ってユーザーを作成したのと同じく
editアクションから送信されたparamsを使って更新する。
構造はかなり似ている。
もちろんparamsでDBを直接更新するのは危険なため今回もStrongParameter(以前定義したuser_paramsメソッド)を使う。def update @user = User.find(params[:id]) if @user.update(user_params) else render 'edit' end end現時点でUserモデルのバリデーションと_error_messagesパーシャルが存在するため
無効な値にはエラーを返してくれるようになっている。演習
編集失敗時のテスト
ユーザーの編集関連の統合テストを作成する。
今回は題目通り編集失敗時のテストを書いていく。
rails g integration_test user_edit
users_edit_test.rbrequire 'test_helper' class UserEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do get edit_user_path(@user) assert_template 'users/edit' patch user_path(@user) , params:{user:{name: "", email: "foo@bar", password: "foo", password_confirmation: "bar"}} assert_template 'users/edit' end end1.editページにGETリクエスト、editページが描画されているか確認
2.updateアクションにpatchリクエスト、無効な値を送信
3.editページが再描画されているか確認。の順のテストになる。
演習
1.
assert_select 'div.alert', "The form contains 4 errors."
TDDで編集を成功させる
今度は成功時の動作を実装していく。
ユーザ⁻画像はGravatarで実装しているためすでに動作する。
name,email,passwordなどほかの属性の編集の成功を実装していく。機能を実装する前に統合テストを書き、機能を実装し終わったときその機能が受け入れ可能な状態かどうか決めるテスト
を「受け入れテスト」と呼ぶ。
実際にTDDで編集成功を実装してみる。先程実装した失敗時のテストを参考に実装していくとわかりやすい。(もちろん有効なデータを送信するが)
test "successful edit" do get edit_user_path(@user) assert_template 'users/edit' name = "foo" email = "foo@bar.com" patch user_path(@user) , params:{user:{name: name, email: email, password: "", password_confirmation: ""}} assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email endもちろんテストは失敗する。
まず、flashメッセージを実装していないこと。リダイレクトの指定をしていないこと。この二つが引っかかる。
そして一番大切な部分。パスワードの値を空にしているため、バリデーションに引っかかり、正常に更新できない。前者の二つはこの行で実装する。
def update @user = User.find(params[:id]) if @user.update(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end endこの時点では@user.updateはパスワードが空欄でバリデーションに引っかかり、else文に分岐するため、
テストもうまく動かない。
この対策としてパスワードが空の時の例外処理を加える。
このような際にはallow_nil: true
というオプションを使うと便利。
これで空欄でもバリデーションにひっかからなくなる。
このオプションで存在性の検証を通過してしまうが、has_secure_password
メソッド側のオブジェクト生成時の
存在性のバリデーションが働くため、新規作成時はnilをはじき、更新時はnilならパスワードを変更しないという
動作を実現できる。
さらにこのallow_nil: true
オプションを追加したことでモデルに定義したバリデーションと
has_secure_password
メソッドのバリデーションが重複して同じエラーメッセージが表示される不具合も解決する。演習
認可
Webアプリにおける認証はユーザーを識別すること。認可はユーザーの実行可能な操作範囲を管理すること。
今まで実装してきたupdate,editアクションでは大きな欠陥があり、
現在の状態ではどのユーザーがログインしていようとすべてのユーザーを編集できてしまう。
ナビゲーションバーのSettingリンクはログインしているユーザーのeditページを表示するが
直接URLに様々なユーザーのeditアクションを指定してしまえばアクセス可能だし、更新もできてしまう。これはまずいので動作を正しいものに変える。
具体的には
未ログイン時はログインページに転送する+メッセージを表示。
ログイン済みだが別のユーザーにアクセスしようとしている場合はルートURLに転送する。ユーザーにログインを要求する
Usersコントローラでbeforeフィルターを使ってedit,updateアクションが実行される前に
必ずログインを強制するよう実装していく。before_action :logged_in_user, only:[:edit,update] . . . def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end endこのように実装することでedit,updateアクションの実行前に必ずlogged_in_userメソッドが実行され
ログインしていない際にはフラッシュメッセージでログインを促すメッセージを表示し、
ログインページにリダイレクトする。そして今の段階ではログインしていない状態でeditビューにアクセスするとログインページに
飛ばされてしまうようになったためテストは失敗する。テストが通るようにuser_edit_test.rbではeditアクションにアクセスする前にログインするようにする。
log_in_asメソッドをテスト用に定義しているためそれをつかう。これでテストはパスするようになる。だがbefore_actionの行をコメントアウトしてもテストではじかれない。
これは重大なセキュリティホールでテストではじかれなければまずいので
しっかりテストではじくよう修正していく。test "should redirect edit when not logged in" do get edit_user_path(@user) assert_not flash.empty? assert_redirected_to login_path end test "should redirect update when not logged in" do patch user_path(@user), params:{ user: {name: @user.name, email: @user.email }} assert_not flash.empty? assert_redirected_to login_path endこのようにテストを追加していくことで
edit,updateアクションを実行する前にlog_in_userが実行されているかを必ずテストするため
セキュリティホールをテストではじいてくれるようになる。演習
1.newページやsignupアクションが実行できなくなりエラーとなる。
FAIL["test_should_get_new", #<Minitest::Reporters::Suite:0x00007f1d1cf4dab8 @name="UsersControllerTest">, 0.06502773099964543] test_should_get_new#UsersControllerTest (0.07s) Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/login> Response body: <html><body>You are being <a href="http://www.example.com/login">redirected</a>.</body></html> test/controllers/users_controller_test.rb:10:in `block in <class:UsersControllerTest>' FAIL["test_invalid_signup_information", #<Minitest::Reporters::Suite:0x00007f1d1cff74c8 @name="UsersSignupTest">, 0.08553676799965615] test_invalid_signup_information#UsersSignupTest (0.09s) expecting <"users/new"> but rendering with <[]> test/integration/users_signup_test.rb:12:in `block in <class:UsersSignupTest>' FAIL["test_valid_signup_information", #<Minitest::Reporters::Suite:0x00007f1d1d0494d0 @name="UsersSignupTest">, 0.09624041300003228] test_valid_signup_information#UsersSignupTest (0.10s) "User.count" didn't change by 1. Expected: 2 Actual: 1 test/integration/users_signup_test.rb:20:in `block in <class:UsersSignupTest>' 9/9: [================================================================] 100% Time: 00:00:01, Time: 00:00:01正しいユーザーを要求する
次はログインしていても本人でなければ編集できないようにしていく。
TDDで進めていく。まずは別のユーザーでログインする状況を作るためにfixtureに2人目のユーザーを追加する。
users.ymlarcher: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %>続いて、テストで@other_userとしてログインし、
@userの更新を行うテストを書く。test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? assert_redirected_to root_path end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params:{ user: { name: @user.name, email: @user.email}} assert flash.empty? assert_redirected_to root_path endflashメッセージは特に表示せずルートURLに飛ばすだけなのでこのようなテストになる。
テストを書き、もちろんパスしないので
テストをパスさせるようにコードを書く。具体的には
correct_user
メソッドを作成し、edit,updateアクション実行前にユーザーがあっていなかったら
ルートURLに飛ばす処理を書く。users_controller.rbbefore_action :correct_user, only:[:edit,:update] private def correct_user @user = User.find(params[:id]) redirect_to root_url unless @user == current_user endこれでテストがパスするようになる。
最後に
current_user?
メソッドを定義し、先ほど定義したcorrect_user
メソッドに組み込むdef correct_user @user = User.find(params[:id]) redirect_to root_url unless current_user?(@user) enddef current_user?(user) user && user == current_user end演習
1.updateアクションを保護しなかった場合、editアクション(editページ)を経由せずcurlコマンドなどで直接
値を送った場合に更新できてしまうから。2.editアクションのほうが簡単にテストできる。(実際にログインして別のユーザのeditパスを表示すればいい)
フレンドリーフォワーディング
さらにこの更新の機能を便利にする。具体的には
logged_in_user
メソッドで未ログインのユーザーのeditページへのアクセスをはじいてログインページに飛ばした際
そのままログインすると問答無用でユーザー詳細ページ(show)に飛ばされてしまうが、
editページにアクセスしたくてログインしたのにshowページが表示されてしまうのは少々不便である。
これを改良しログインしたらeditページに飛ばしてくれるようにする(フレンドリーフォワーディング)テストもこの通り実装すればいいので
未ログインでeditページにアクセスし、ログインしたらeditページにリダイレクトすることをチェックする。test "successful edit with friendly forwarding" do get edit_user_path(@user) log_in_as(@user) assert_redirected_to edit_user_url(@user) name = "foo" email = "foo@bar.com" patch user_path(@user) , params:{user:{name: name, email: email, password: "", password_confirmation: ""}} assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end現時点で失敗するテストが書けたのでつぎはこのテストがパスするようにコードを書いていく。
リクエスト時のページを保存しておき、ログイン時にそこへリダイレクトする処理を書く。sessions_helperにメソッドを定義する。
sessions_helper.rbdef redirect_back_or(default) redirect_to(session[:forwarding_url] || default) session.delete(:forwarding_url) end def store_location session[:forwarding_url] = request.original_url if request.get? end一時セッションにリクエスト先のURLを保存する処理を書いている。 この時GETリクエストだけを保存するようにしないと 万が一ログインしてフォームページににアクセスし、意図的に保存されたログイン情報のcookiesを削除した上で フォームの内容を送信するとpost,patchなどのURLが保存されてしまう。 その状態で```redirect_back_or```メソッドを使うとリダイレクトでpost,patchなどを期待するURLに対してGETリクエストが 送られてしまい、エラーが発生する可能性が高い。 GETリクエストに絞ることでこういったリスクを回避できる。 ```logged_in_user```メソッドに```store_location```メソッドを入れて、リクエスト先URLを保存し、 sessions_controllerのcreateアクションに```redirect_back_orメソッド```を入れることで、 ログイン時、もしセッションに保存されたURLがあればそちらにリダイレクトするようにする。 ```rb:sessions_controller.rb def create @user = User.find_by(email: params[:session][:email].downcase) if @user&.authenticate(params[:session][:password]) log_in(@user) params[:session][:remember_me] == '1' ? remember(@user) : forget(@user) redirect_back_or @user else flash.now[:danger] = "Invalid email/password combination" render 'new' end endusers_controller.rbdef logged_in_user unless logged_in? store_location flash[:danger] = "Please log in." redirect_to login_url end endちなみにreturnやメソッドの最終行の直接呼出しがない限りは
リダイレクトはメソッドの最後で行われる。上記の内容でテストはパスする。
演習
1.ログインしてeditページにリダイレクトした時点で保存したURLが消えていることを確認すればいい。
user_edit_test.rbtest "successful edit with friendly forwarding" do get edit_user_path(@user) log_in_as(@user) assert_nil session[:forwarding_url] #ここを追加 assert_redirected_to edit_user_url(@user) name = "foo" email = "foo@bar.com" patch user_path(@user) , params:{user:{name: name, email: email, password: "", password_confirmation: ""}} assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end2.
(byebug) session[:forwarding_url] "https://12b7e3b6aec94b45960b81560e233372.vfs.cloud9.us-east-2.amazonaws.com/users/1/edit" (byebug) request.get? true最後に
だんだん章の最後にやることが増えてきたので
とりあえずまとめておく。rails t git add -A git commit -m "Finish user edit, update, index and destroy actions" git co master git merge updating-users git push rails t git push heroku heroku pg:reset DATABASE heroku run rails db:migrate heroku run rails db:seed本番用DBは
pg:reset DATABASE
で行う。なお、間違いを防ぐためにDBをリセットするアプリ名の
入力を求められるので入力してリセット
それか--confirmオプションを使って
heroku pg:reset DATABASE -c アプリ名
としてもいい。あとはheroku上でマイグレートとサンプルの追加を行って終わり。
- 投稿日:2020-06-25T21:18:38+09:00
Rails初心者から中級者までに伝えたい、Controllerのアンチパターン3つ
tl; dr
controllerでは特に、インスタンス変数の濫用はやめよう!
すべてのはじまり
ほとんど触ったことないプロジェクトに移籍させられ早1ヶ月、そのコードを知る熟練の先輩も去った荒廃の地で、私は今日も一人、Railsと激闘を繰り広げていた!
というわけで(?)、今回はcontrollerでやってはいけないアンチパターンを紹介していきたい。クソコード1. インスタンス変数に入れればええやろ
これは初心者がやらかしがちな印象
こういうやつsample_controller.rbclass SampleController < ApplicationController def show hoge = Hoge.new @hoge_kuso1 = hoge.kuso @hoge_kuso2 = hoge.sugoku_kuso @hoge_kuso3 = Hoge.fetch_kuso end end......
こ☆ろ☆す☆ぞ
まず第一に、Railsのコントローラーは一つのディレクトリに入るものをだいたい一つのクラスで固めるので、言い換えれば複数のアクションのロジックが一つのクラスにまとまっている。なので例えば次のようなコードは日常茶飯事である。
sample_controller.rbclass SampleController < ApplicationController def action1 end def action2 end endこの状態で2つのアクションがインスタンス変数をたくさん使ったとしよう。それが以下
sample_controller.rbclass SampleController < ApplicationController def action1 hoge = Hoge.new @hoge_kuso1 = hoge.kuso @hoge_kuso2 = Hoge.fetch_kuso end def action2 hoge = Hoge.new @hoge_kuso1 = hoge.sugoku_kuso @hoge_kuso2 = Hoge.fetch_kuso end ... endついでに
Hoge
クラスはこうhoge.rbrequire 'rest-client' class Hoge def kuso :kuso end def sugoku_kuso "sugoku kuso" end def self.fetch_kuso RestClient.get('https://kuso.com/kuso3').body rescue 'honma kuso' end endこの状態の何が悪いのかというと、インスタンス変数をたくさん使ったことにより、コード全体でどの変数がどこに所属しているのかが分かりにくいことだ。例えば
@hoge_kuso1
はクラス内スコープの変数であるので、もしかしたら他のところの定義とコンフリクトしてバグるかもしれない。それにクラス内検索しても複数の全然関係ない奴が引っかかる可能性もあるので、補足もしづらい。あんまりメリットはないのである。解決策
単純な話、極力インスタンス変数を使わないことだ。具体的にはこう。
sample_controller.rbclass SampleController < ApplicationController def action1 @hoge = Hoge.new end def action2 @hoge = Hoge.new end ... endhoge.rbrequire 'rest-client' class Hoge def kuso :kuso end def sugoku_kuso "sugoku kuso" end # 要素をモデルからアクセスできるようにして、controller上でインスタンス変数を使う可能性をを極力排除する def fetched_kuso @fetched_kuso ||= self.class.fetch_kuso end def self.fetch_kuso RestClient.get('https://kuso.com/kuso3').body rescue 'honma kuso' end endどういうことかというと、インスタンス変数に入れるのはモデル程度にしておいて、詳細なデータはモデル内に内包してしまおう、ということだ。これのメリットは2つ
- コントローラーがスッキリする
- ロジックが複数の場所に依らない
正直メリットしかないので、やらない手はないと思う。ちなみにたとえば複数のモデルに対して一括で値を取得したいんだけど、その場合はどうするの?ということについては
sample_controller.rbclass SampleController < ApplicationController def action1 @hoges = 10.times.map { Hoge.new } Hoge.set_kusos(@hoges) end endhoge.rbrequire 'rest-client' class Hoge def kuso :kuso end def sugoku_kuso "sugoku kuso" end attr_accessor :fetched_kuso # 専用のセッター def self.set_kusos(hoges) hoges.each do |hoge| hoge.fetched_kuso = self.fetch_kuso end end def self.fetch_kuso RestClient.get('https://kuso.com/kuso3').body rescue 'honma kuso' end endとするなどして、モデル内にデータを保持するといい。とにかくコントローラーで無闇にロジックを書かないようにすることは大事である。
クソコード2. set_〇〇って書いとけば安泰やろ。其の壱
ダメです
例えばこう書く人はほとんどだと思うsample_controller.rbclass SampleController < ApplicationController before_action :set_hoge, only: :action1 def action1 end private def set_hoge @hoge = Hoge.new end end絶対10人に9人はこう書いてる。なぜならscaffoldでもこう書くのを推奨してるから。
じゃあこれの何がダメなのかというと、以下の2つ
- コードが追いにくい
- 引数を渡しにくい
まず1について
sample_controller.rbclass SampleController < ApplicationController before_action :set_hoge, only: :action1 before_action :set_hoges, only: :action2 def action1 end def action2 end private def set_hoge @hoge = Hoge.new end def set_hoges @hoges = [Hoge.new, Hoge.new] end endこう書かれた場合に、action1を確認した瞬間、あ、これは
@hoge
を使うんだな、ってなる人はいないだろう。
少なくとも、
action1がなにも定義されていない -> あ、before_actionが定義されているということはつまり -> やっぱり@hoge
を使っていたか
となるのではないだろうか?
これはアハ体験としては適切な体験かもしれないが、コーディングに脳科学を持ち込まないでほしい。はっきり言ってストレスである。また、 クソコード1でも述べたように、コントローラー内で使用されるインスタンス変数は所在がわかりにくくなりがちである。よってインスタンス変数を無闇に使うのはよろしくない。
次に2について
sample_controller.rbclass SampleController < ApplicationController before_action(only: :action1) do set_hoge('fuga') end def action1 end private def set_hoge(fuga) @hoge = Hoge.new(fuga) end endこうしないと変数が渡せない。逆にこれだったら渡せるじゃん?という諸君。確かに渡せるが、これだとせっかく便利なskip_before_actionが利用できない。そこまでしてset_〇〇を使う必要ある?という気持ちでいっぱいである。
解決策
答えは単純である。
sample_controller.rbclass SampleController < ApplicationController def action1 @hoge = fetch_hoge end private def fetch_hoge Hoge.new end endこれであれば、インスタンス変数がロジック内に組み込まれているので、見た瞬間に
@hoge
を使うことがわかる。また引数を渡すのも容易である。とってもスマート、故にハッピー。また、これだとredirect_toを書くのに手間がかかるじゃん!そっちはどうすんのさ!?っていうみなさんのために、以下のコードを授けよう
sample_controller.rbclass SampleController < ApplicationController class DataNotFound < RuntimeError; end rescue_from DataNotFound do redirect_to :root_path, flash: { error: 'データが見つかりませんでした' } end def action1 @hoge = fetch_hoge end private def fetch_hoge hoge = Hoge.new raise DataNotFound unless hoge hoge end endこれであれば元々の機能を維持できるだろう。
クソコード3. set_〇〇って書いとけば安泰やろ。其の弐
ダメだって言ってんだろ!!!
場合にもよるけど、例えばヘッダーで使う値をこうやって作る人いるよねsample_controller.rbclass SampleController < ApplicationController before_action :set_header_de_tukau_yatu def set_header_de_tukau_yatu @header_info = 'void' end endこれの問題点は「
@header_info
を利用する」ためには、「:set_header_de_tukau_yatu
を呼び出す」必要があることを知っていなきゃいけないことだ。この場合、viewがほしいのは@header_info
だけ、なのになぜその設定方法までこっちが把握してなきゃいかんの?っていう話解決策
こういうときにどうすればいいのかというと helper_methodを使う。例えば以下
sample_controller.rbclass SampleController < ApplicationController helper_method :header_info def header_info @header_info ||= 'void' end endこうすることで、viewは
header_info
を呼び出すと、情報が得られる。ということを知っているだけでいい。わざわざ内部のロジックまで知る必要もないし、インスタンス変数もロジックに近い場所にある。とっても素晴らしい。おわりに
この記事ではアンチパターンを3つしか紹介していないが、多分世の中にはもっとたくさんのアンチパターンがあると思う。しかし多くの場合、それは「可読性が下がる」「使い勝手が悪い」のいずれかないし両方を満たしているのではなかろうか?そしてその二つを生み出しがちな諸悪の根源、それはインスタンス変数の濫用である(暴論。今回本当に伝えたかったことはただ一つ。みなさんご唱和ください。
インスタンス変数の濫用はやめよう!!!!!
読了ありがとうございました。
- 投稿日:2020-06-25T21:18:38+09:00
Rails初心者から中級者までに伝えたい、アンチパターン3つ
tl; dr
controllerでは特に、インスタンス変数の濫用はやめよう!
すべてのはじまり
ほとんど触ったことないプロジェクトに移籍させられ早1ヶ月、そのコードを知る熟練の先輩も去った荒廃の地で、私は今日も一人、Railsと激闘を繰り広げていた!
というわけで(?)、今回はcontrollerでやってはいけないアンチパターンを紹介していきたい。クソコード1. インスタンス変数に入れればええやろ
これは初心者がやらかしがちな印象
こういうやつsample_controller.rbclass SampleController < ApplicationController def show hoge = Hoge.new @hoge_kuso1 = hoge.kuso @hoge_kuso2 = hoge.sugoku_kuso @hoge_kuso3 = Hoge.fetch_kuso end end......
こ☆ろ☆す☆ぞ
まず第一に、Railsのコントローラーは一つのディレクトリに入るものをだいたい一つのクラスで固めるので、言い換えれば複数のアクションのロジックが一つのクラスにまとまっている。なので例えば次のようなコードは日常茶飯事である。
sample_controller.rbclass SampleController < ApplicationController def action1 end def action2 end endこの状態で2つのアクションがインスタンス変数をたくさん使ったとしよう。それが以下
sample_controller.rbclass SampleController < ApplicationController def action1 hoge = Hoge.new @hoge_kuso1 = hoge.kuso @hoge_kuso2 = Hoge.fetch_kuso end def action2 hoge = Hoge.new @hoge_kuso1 = hoge.sugoku_kuso @hoge_kuso2 = Hoge.fetch_kuso end ... endついでに
Hoge
クラスはこうhoge.rbrequire 'rest-client' class Hoge def kuso :kuso end def sugoku_kuso "sugoku kuso" end def self.fetch_kuso RestClient.get('https://kuso.com/kuso3').body rescue 'honma kuso' end endこの状態の何が悪いのかというと、インスタンス変数をたくさん使ったことにより、コード全体でどの変数がどこに所属しているのかが分かりにくいことだ。例えば
@hoge_kuso1
はクラス内スコープの変数であるので、もしかしたら他のところの定義とコンフリクトしてバグるかもしれない。それにクラス内検索しても複数の全然関係ない奴が引っかかる可能性もあるので、補足もしづらい。あんまりメリットはないのである。解決策
単純な話、極力インスタンス変数を使わないことだ。具体的にはこう。
sample_controller.rbclass SampleController < ApplicationController def action1 @hoge = Hoge.new end def action2 @hoge = Hoge.new end ... endhoge.rbrequire 'rest-client' class Hoge def kuso :kuso end def sugoku_kuso "sugoku kuso" end # 要素をモデルからアクセスできるようにして、controller上でインスタンス変数を使う可能性をを極力排除する def fetched_kuso @fetched_kuso ||= self.class.fetch_kuso end def self.fetch_kuso RestClient.get('https://kuso.com/kuso3').body rescue 'honma kuso' end endどういうことかというと、インスタンス変数に入れるのはモデル程度にしておいて、詳細なデータはモデル内に内包してしまおう、ということだ。これのメリットは2つ
- コントローラーがスッキリする
- ロジックが複数の場所に依らない
正直メリットしかないので、やらない手はないと思う。ちなみにたとえば複数のモデルに対して一括で値を取得したいんだけど、その場合はどうするの?ということについては
sample_controller.rbclass SampleController < ApplicationController def action1 @hoges = 10.times.map { Hoge.new } Hoge.set_kusos(@hoges) end endhoge.rbrequire 'rest-client' class Hoge def kuso :kuso end def sugoku_kuso "sugoku kuso" end attr_accessor :fetched_kuso # 専用のセッター def self.set_kusos(hoges) hoges.each do |hoge| hoge.fetched_kuso = self.fetch_kuso end end def self.fetch_kuso RestClient.get('https://kuso.com/kuso3').body rescue 'honma kuso' end endとするなどして、モデル内にデータを保持するといい。とにかくコントローラーで無闇にロジックを書かないようにすることは大事である。
クソコード2. set_〇〇って書いとけば安泰やろ。其の壱
ダメです
例えばこう書く人はほとんどだと思うsample_controller.rbclass SampleController < ApplicationController before_action :set_hoge, only: :action1 def action1 end private def set_hoge @hoge = Hoge.new end end絶対10人に9人はこう書いてる。なぜならscaffoldでもこう書くのを推奨してるから。
じゃあこれの何がダメなのかというと、以下の2つ
- コードが追いにくい
- 引数を渡しにくい
まず1について
sample_controller.rbclass SampleController < ApplicationController before_action :set_hoge, only: :action1 before_action :set_hoges, only: :action2 def action1 end def action2 end private def set_hoge @hoge = Hoge.new end def set_hoges @hoges = [Hoge.new, Hoge.new] end endこう書かれた場合に、action1を確認した瞬間、あ、これは
@hoge
を使うんだな、ってなる人はいないだろう。
少なくとも、
action1がなにも定義されていない -> あ、before_actionが定義されているということはつまり -> やっぱり@hoge
を使っていたか
となるのではないだろうか?
これはアハ体験としては適切な体験かもしれないが、コーディングに脳科学を持ち込まないでほしい。はっきり言ってストレスである。また、 クソコード1でも述べたように、コントローラー内で使用されるインスタンス変数は所在がわかりにくくなりがちである。よってインスタンス変数を無闇に使うのはよろしくない。
次に2について
sample_controller.rbclass SampleController < ApplicationController before_action(only: :action1) do set_hoge('fuga') end def action1 end private def set_hoge(fuga) @hoge = Hoge.new(fuga) end endこうしないと変数が渡せない。逆にこれだったら渡せるじゃん?という諸君。確かに渡せるが、これだとせっかく便利なskip_before_actionが利用できない。そこまでしてset_〇〇を使う必要ある?という気持ちでいっぱいである。
解決策
答えは単純である。
sample_controller.rbclass SampleController < ApplicationController def action1 @hoge = fetch_hoge end private def fetch_hoge Hoge.new end endこれであれば、インスタンス変数がロジック内に組み込まれているので、見た瞬間に
@hoge
を使うことがわかる。また引数を渡すのも容易である。とってもスマート、故にハッピー。また、これだとredirect_toを書くのに手間がかかるじゃん!そっちはどうすんのさ!?っていうみなさんのために、以下のコードを授けよう
sample_controller.rbclass SampleController < ApplicationController class DataNotFound < RuntimeError; end rescue_from DataNotFound do redirect_to :root_path, flash: { error: 'データが見つかりませんでした' } end def action1 @hoge = fetch_hoge end private def fetch_hoge hoge = Hoge.new raise DataNotFound unless hoge hoge end endこれであれば元々の機能を維持できるだろう。
クソコード3. set_〇〇って書いとけば安泰やろ。其の弐
ダメだって言ってんだろ!!!
場合にもよるけど、例えばヘッダーで使う値をこうやって作る人いるよねsample_controller.rbclass SampleController < ApplicationController before_action :set_header_de_tukau_yatu def set_header_de_tukau_yatu @header_info = 'void' end endこれの問題点は「
@header_info
を利用する」ためには、「:set_header_de_tukau_yatu
を呼び出す」必要があることを知っていなきゃいけないことだ。この場合、viewがほしいのは@header_info
だけ、なのになぜその設定方法までこっちが把握してなきゃいかんの?っていう話解決策
こういうときにどうすればいいのかというと helper_methodを使う。例えば以下
sample_controller.rbclass SampleController < ApplicationController helper_method :header_info def header_info @header_info ||= 'void' end endこうすることで、viewは
header_info
を呼び出すと、情報が得られる。ということを知っているだけでいい。わざわざ内部のロジックまで知る必要もないし、インスタンス変数もロジックに近い場所にある。とっても素晴らしい。おわりに
この記事ではアンチパターンを3つしか紹介していないが、多分世の中にはもっとたくさんのアンチパターンがあると思う。しかし多くの場合、それは「可読性が下がる」「使い勝手が悪い」のいずれかないし両方を満たしているのではなかろうか?そしてその二つを生み出しがちな諸悪の根源、それはインスタンス変数の濫用である(暴論。今回本当に伝えたかったことはただ一つ。みなさんご唱和ください。
インスタンス変数の濫用はやめよう!!!!!
読了ありがとうございました。
- 投稿日:2020-06-25T20:18:49+09:00
rails routes(ルーティング)の順番の解説
自分用のまとめ
https://qiita.com/gawach/items/4e7460d06f70e3013eea
にてまとめられているが、私は違う理解の仕方をしている。
それを説明します。⑴ 1 get "posts/:id" => "posts#show" 2 get "posts/new" => "posts#new"⑵ 3 get "posts/new" => "posts#show" 4 get "posts/:id" => "posts#new"⑶(プロゲートに書いてあった正しいコード) get "posts/index" => "posts#index" get "posts/:id" => "posts#show"⑴は間違いであり、⑵は実は正しいと決める、それを下記に示す。
上から順にコードを優先していくため、
:id以外の特定のURLが:idより先にくるようにする。
そしてそれら以外の余ったものが:idの対象となる。
上記のようなルールがあるため、
:idはposts/newのようなURLより先に記述されているとエラーになる。
- 投稿日:2020-06-25T20:15:51+09:00
【Rails】lazy_high_chartsを用いてグラフを作成する方法
目標
開発環境
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina前提
下記実装済み。
・Slim導入
・Bootstrap3導入
・Font Awesome導入
・ログイン機能実装
・投稿機能実装実装
1.Gemを導入
Gemfile# 追記 gem 'lazy_high_charts'ターミナル$ bundle2.
application.js
を編集application.js//= require rails-ujs //= require activestorage //= require turbolinks //= require highcharts/highcharts // 追記 //= require highcharts/highcharts-more // 追記 //= require_tree .3.コントローラーを編集
books_controller.rbdef index @book = Book.new @books = Book.all # 追記 days = (Date.today.beginning_of_month..Date.today).to_a books = days.map { |item| Book.where(created_at: item.beginning_of_day..item.end_of_day).count } @graph = LazyHighCharts::HighChart.new('graph') do |f| f.title(text: '本 月間登録推移') f.xAxis(categories: days) f.series(name: '登録数', data: books) end end① 今月1日から今日までの日付を取得し、変数へ代入する。
days = (Date.today.beginning_of_month..Date.today).to_a②
①
で取得した日付内に作成された本の数を取得し、変数へ代入する。books = days.map { |item| Book.where(created_at: item.beginning_of_day..item.end_of_day).count }③ グラフを作成する
@graph = LazyHighCharts::HighChart.new('graph') do |f| f.title(text: '本 月間登録推移') # タイトル f.xAxis(categories: days) # 横軸 f.series(name: '登録数', data: books) # 縦軸 end4.ビューを編集
books/index.html.slim/ 追記 = high_chart('sample', @graph)
- 投稿日:2020-06-25T19:28:01+09:00
Cloud9がメモリー足りないってよ: Railsチュートリアル備忘録
Cloud9がメモリー足りないってよ: Railsチュートリアル備忘録
Cloud9に以下のメッセージが出てコンソールが固まる事態に
This environment is running low on memory (132 MB of virtual memory left).
To free up memory, kill processes by choosing "Tools > Process List" in the menu bar.
To migrate this environment to one that has more memory, ...解決法
EC2インスタンスを再起動する
経緯
Railsチュートリアル13章進行中のこと
トリガーもよくわからいのですがTestを走らせると... Missing helper file helpers/microposts_helper.rb ...
といったエラーのが出るように
bundle update
するといいよとの記事があったため
試してみたところCloud9上で上記通知がでるように...
ググってみてもあまり情報がなく
いっそ試してみたかったDockerを利用して環境再構築...なんて考えましたが結局EC2インスタンスを再起動することで2つとも解決しました
インスタンスの再起動 - Amazon Elastic Compute Cloud
いまさらですがオンラインIDEであるCloud9は
AWSの一部であるEC2サービスで提供される
仮想サーバー(これがEC2インスタンス)上で動いているわけですねインスタンスという言葉がすっと頭に馴染むようになってきたこととに喜び、
AWSの全体像について視野が広がったという学びでした
- 投稿日:2020-06-25T19:04:03+09:00
Gem simple calendarでのカレンダー実装と条件分岐
概要
現在DMMWEBCAMPというプログラミングスクールで学習しておりまして、
ポートフォリオ作成にあたりイベントテーブルを作成してカレンダーに表示させました。
これから作成する方のお役に立てればと思います。完成図
参考にさせて頂いた記事
【Rails】Simple Calendarを使ったカレンダー表示
https://qiita.com/mikaku/items/0971442a7308dc669122github
https://github.com/excid3/simple_calendar事前準備
イベントテーブル
・data_and_timeがスタートする日時
・meetingfinishtimeが終了する日時イベント参加テーブル
アソシエーション
models/event.rbclass Event < ApplicationRecord has_many :event_participates, dependent: :destroy endmodels/event_participate.rbclass EventParticipate < ApplicationRecord belongs_to :event endgem install
gem 'simple_calendar', '~>2.0'$ bundle installSimple calendarのViewインストール
$ rails g simple_calendar:views・views/simple_calendar内に3種類のカレンダーの雛形が出来ます。
application.scss(CSS)への記載
stylesheets/applecation.scss*= require simple_calendar *= require_tree . *= require_selfstart_time end_timeの定義
models/event.rbclass Event < ApplicationRecord has_many :event_participates, dependent: :destroy def start_time self.date_and_time end def end_time self.meetingfinishtime end endこのgemではstart_timeとend_timeの定義が必要です。(特にstart_time)
作成しているカラムが最初からその名前であれば良いのですが、違うカラム名なのであればモデルの方で定義しましょう。ただし日付と時刻を扱うのでデータ型は恐らくdatetime型でなければならないと思います。
最初定義せずに実装したところエラーが出ました。githubではscaffoldを使用して最初から上記名称でカラムを作成しておりました。
カレンダーの表示
私のポートフォリオ
views/events/index.html.erb</div> <div class= 'calender'> <%= month_calendar events: @events do |date, event| %> <%= date %> <% event.each do |event| %> <br> <% if user_signed_in? %> <% if event.user_id == current_user.id %> <i class="fa fa-circle notification-circle" style="color: red;"></i> <% end %> <% end %> <% if event.event_participates.where(user_id: current_user).exists? %> <span class="participate"> <%= link_to event.title,event_path(event) %> </span> <% else %> <span class="other"> <%= link_to event.title,event_path(event) %> </span> <% end %> <% end %> <% end %> </div>カレンダーの表示とイベントのタイトルだけであれば以下のみで良いです。
<div class= 'calender'> <%= month_calendar events: @events do |date, event| %> <%= date %> <% event.each do |event| %> <%= event.title %> <% end %> <% end %> </div>controllers/events_controller.rbdef index @events = Event.all endコントローラーで定義しているEvent.allを@eventsに格納しカレンダーで表示しています。
<%= month_calendar events: @events
この構文の中のevents:は恐らく変えるとエラーが出ますが、インスタンス変数は情報が格納されていればどんなものを使用しても大丈夫だと思います。現在は先ほどインストールしたmonth_calendarを使用していますが、他に2つテンプレートが用意されているので3種類から使用することが出来るようです。
私は参加(participate)アクションを取っていればbackground-colorをオレンジに、
主催しているイベントであれば赤丸のアイコンを付けたかったので以下のような条件分岐を
カレンダー内で行いました。<% if event.user_id == current_user.id %> <i class="fa fa-circle notification-circle" style="color: red;"></i> <% end %>イベントテーブルの中のuser_id(つまりイベント作成者)が自分であればイベントタイトルの前にfont awesomeのアイコンを表示というものです。インライン要素のものであればここは何でも良いと思います。
次に
<% if event.event_participates.where(user_id: current_user).exists? %> <span class="participate"> <%= link_to event.title,event_path(event) %> </span> <% else %> <span class="other"> <%= link_to event.title,event_path(event) %> </span> <% end %>1行目の <% if event.event_participates.where(user_id: current_user).exists? %>で
eventに参加(participete)するユーザーの中に自分(current_user)が存在しているかどうかを探しています。
見つかればclass participate、そうでなければclass otherとすることで、
表示する情報は同じでもclass名が違うという状態にすることが出来ました。後はCSSで色を変えていくだけなので、皆さんのお好みのようにしていただければと思います。
ちなみにmonth_calenderのviewは以下です。
views/simple_calendar/_month_calendar.html.erb<div class="simple-calendar"> <p class="carendar-title"><%= "#{Date.today.month}月のイベントスケジュール" %></p> <div class="calendar-heading" data-turbolinks="false"> <!--%= link_to t('simple_calendar.previous', default: 'Previous'), calendar.url_for_previous_view %--> <!-- <span class="calendar-title"><%= t('date.month_names')[start_date.month] %> <%= start_date.year %></span> --> <!--%= link_to t('simple_calendar.next', default: 'Next'), calendar.url_for_next_view %--> <i class="fa fa-circle notification-circle" style="color: red;"><span>主催イベント</span></i> <span class="calendar-info">参加予定</span> </div> <table class="table table-striped calendar-table"> <thead> <tr> <% date_range.slice(0, 7).each do |day| %> <th><%= t('date.abbr_day_names')[day.wday] %></th> <% end %> </tr> </thead> <tbody> <% date_range.each_slice(7) do |week| %> <tr> <% week.each do |day| %> <%= content_tag :td, class: calendar.td_classes_for(day) do %> <% if defined?(Haml) && respond_to?(:block_is_haml?) && block_is_haml?(passed_block) %> <% capture_haml(day, sorted_events.fetch(day, []), &passed_block) %> <% else %> <% passed_block.call day, sorted_events.fetch(day, []) %> <% end %> <% end %> <% end %> </tr> <% end %> </tbody> </table> </div>私のポートフォリオの仕様で上の部分をコメントアウトしています。
コメントアウトの場所に本来であれば次月や前月へのリンクが出てきます。
細かいところであれば柔軟に変えれそうです。まとめ
最初はfullcalendarでの実装を目論んだのですが、理解が及ばず諦めて今回こちらのgemを使用しました。イベントタイトルがstart_timeとend_timeの間で毎回出てしまうことを改善できればと思ったのですが私の力不足で難しかったです。
要素のclass名に特に意味はありません。スペルを所々間違えてますが、、
このぐらい簡単なカレンダーでも見せ方次第で私のものよりもっといいものになると思うので是非やってみてください。
- 投稿日:2020-06-25T18:56:55+09:00
Ruby on Rails MVC周りのリファクタリング方法例まとめ
何も考えずにやりたいように記述していると増えていくコード量、下がる可読性……
なるべく簡潔に纏められるよう、MVCそれぞれで使える手段を初学者なりにまとめました。MVCの役割をおさらい
MVC 役割 Model アプリケーションで扱うデータの操作/加工 View ユーザへの情報表示、ユーザからの入力の受け取り Controller ModelやViewとの情報のやり取りをする この役割を理解し、それぞれの役割に相応しい処理を記述していく。
役割を考えず好きに各ファイルに記述していると、メンテナンスも難しくなってきてしまう。
例えば、Modelに記述すべき処理をControllerに記述してしまうとFat Controller
(コードが大量に書かれて膨れ上がったController)になるなどの問題が発生しやすくなる。Controllerのコード量を減らす方法
Modelに記述する。
上記のMVCの各役割を考えて、データの処理・加工を担うメソッドはModelに記述する。
訂正前
app/controllers/posts_controller.rbclass PostsController < ApplicationController def index @posts = Post.order(created_at: :desc).limit(10) end end訂正後(Modelに移動)
Modelクラスに対して直接呼び出せるようクラスメソッドとして定義。
app/models/post.rbclass Post < ApplicationRecord def self.latest(number) order(created_at: :desc).limit(number) end endControllerでメソッドを呼び出す
app/controllers/posts_controller.rbclass PostsController < ApplicationController def index @posts = Post.latest(10) end endクラスメソッドでなく、scopeを使うことも出来る。
app/models/post.rbclass Post < ApplicationRecord scope :latest, -> (number = 10){order(created_at: :desc).limit(number)} endapp/controllers/posts_controller.rbclass PostsController < ApplicationController def index @posts = Post.latest end endModelのコード量を減らす方法
Concernを使う
複数のModelに同じ処理が繰り返し記述されている場合などに、Concernを使って共通処理を切り出すことが出来る。
app/models/post.rbscope :latest, -> (number = 10){order(created_at: :desc).limit(number)} def hoge puts "hogehoge" endapp/models/comment.rbscope :latest, -> (number = 10){order(created_at: :desc).limit(number)} def hoge puts "hogehoge" endapp/models/concernsにファイルを作成・共通処理を記述。
app/models/concerns/common_module.rbmodule CommonModule extend ActiveSupport::Concern included do scope :latest, -> (number = 10){order(created_at: :desc).limit(number)} def self.hoge puts "hogehoge" end end end各Modelでincludeすることで共通処理が使用できる。
app/models/post.rbinclude CommonModuleapp/models/comment.rbinclude CommonModuleバリデーションを切り出す
長くなりがちなバリデーションの記述をvalidatorに切り出すことが出来る。
appディレクトリ下にvalidatorsディレクトリを作成し、バリデーション用のファイルを作成する。app/validators/post_validator.rbclass PostValidator < ActiveModel::Validator def validate(record) prohibited_words = ["baka","aho"] if prohibited_words.any?{ |word| record.content.include?(word) } record.errors.add(:content, "禁止ワードが含まれています。") end end endModel側に追加で記述する。
app/models/post.rbvalidates_with PostValidatorコールバックを切り出す
コールバックもModelから切り出すことが出来る。
app/callbacksディレクトリを作成し、コールバック用のファイルを作成する。
(クラス名はコールバック名と同じにすること)app/callbacks/post_callback.rbclass PostCallback def before_create(post) post.title = "No Title" if post.title.blank? end endModel側に追加で記述する。
app/models/post.rbbefore_create PostCallback.new
- 投稿日:2020-06-25T18:16:00+09:00
Rails×Heroku環境でSSL化したときhttp->httpsにリダイレクトさせる方法
config/environments/production.rb# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true # ←ここがコメントアウトされてるので解除するだけ公式ドキュメント
https://help.heroku.com/J2R1S4T8/can-heroku-force-an-application-to-use-ssl-tls
- 投稿日:2020-06-25T18:00:05+09:00
Rails6〜の環境構築にて、データベースにMySQLを使いたいそんなあなたに。
そもそもMySQLとmysql2は、別物
MySQLは皆さんご存知、データベースなんですが、
mysql2ってのはRails専用のgemのことで、データベースとRailsを繋いでくれる架け橋と解釈しています。mysql2のエラーについて
$ bundle install --path vendor/bundle An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue. Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling. $ gem install mysql2 -v '0.5.3' ERROR: While executing gem ... (Gem::FilePermissionError) You don't have write permissions for the /Library/Ruby/Gems/2.3.0 directory.そして、さらにbundle installしても、、、という「お、俺のそばに近寄るなァァァ!!」と無限ループの住民になってしまったあなた。
私もそこの住人でしたが、安心してください、脱出できます笑
解決方法
結論、mysql2をインストールしたことを無かったことにすれば良いのです。
ここで、出てくるのがおなじみGemfileと、Gemfile.lockです。
Gemfileはいうならば、ゲームソフトを買いに行く時の「買うものリスト」で、
Gemfile.lockは、「領収書」です。bundle installするときは、この買う物リストにあるソフトを見てgemをインストールするのですが、
今エラーの原因になってるのは、「領収書にあるゲームソフトをまた買いに行くの?」ってなってる状態なんです。ゲームソフトを遊ぶ用、観賞用、保存用としている方なら別ですが笑
それは、おいときまして。ということで、買ったことを無かったことにてして、新シリーズ版を買いなおしましょう。という話です。
とはいってもやることは、2つ。$ gem uninstall mysql2Gemfile.lockmysql2と名のつくものを削除そして、買いなおしましう!
$ bundle install --path vendor/bundle
- 投稿日:2020-06-25T16:59:44+09:00
Ruby Minitestを試す
目的
Railsのデフォルトのテスティングフレームについてアウトプットする。
テストコードのひな形
Minitestを使ったテストコードのひな形は下記の通り。
sample_test.rbrequire 'minitest/autorun' class SampleTest < Minitest::Test def test_sample assert_equal 5, 'こんにちは'.length end end・1行目でMinitestに必要なライブラリを呼び出す。
・SampleTestクラスにMinitest::Testクラスを継承させる。
・SampleTestクラス内に、実行対象となるテストメソッドを定義する(test_sample)
という流れで作成できる。assert_equalは検証メソッドで、第1引数には予想される値、第2引数は検証する値を入れる。
したがって、test_sampleメソッドは、「'こんにちは'という文字列の文字数」が「5文字」であるかをテストしている。
結果として等しくなるので、テストはパスする。
・runs: 実行したテストメソッドの件数(test_sampleのみなので1件)
・assertions: 実行した検証メソッドの件数(assert_equalを1回使用)
・failures: 検証に失敗したテストメソッドの件数
・errors: 検証中にエラーが出たテストメソッドの件数
・skips: skipメソッドにより実行をスキップされたテストメソッドの件数
を表している。検証メソッドの例
http://docs.seattlerb.org/minitest/Minitest/Assertions.html
MinitestのAPIドキュメントを参考にいくつか検証メソッドを使ってみる。hello_test.rbrequire 'minitest/autorun' def hello(name) puts "#{name}さん、こんにちは!" end class HelloTest < Minitest::Test def test_hello assert_output("Bobさん、こんにちは!\n") { hello("Bob") } end endassert_outputを使うと、標準出力した内容をテストできる。
multiple_of_eleven_test.rbrequire 'minitest/autorun' def multiple_of_eleven?(number) number % 11 == 0 end class MultipleOfElevenTest < Minitest::Test def test_true assert multiple_of_eleven? 121 end def test_false refute multiple_of_eleven? 13 end endassert,refuteメソッドを使うと、真偽をテストできる。
後書き
Minitestを使うと簡単にテストを実装できるので、活用してみる。
今度はRspecについてもアウトプットしたい。参考文献
プロを目指す人のためのRuby入門
伊藤 淳一[著]
- 投稿日:2020-06-25T16:01:03+09:00
slimの始め方
slimのインストール
アプリのフォルダ内でslimをインストールする。
gem install slimこれで拡張子が.html.slimのslimファイルを、みなさんが普段使っている.html.erbとして変換されるようになるそうです。
gem install html2slimこれでhtml.erb → html.slim に変換させることができるようになるそうです。
bundle exec erb2slim app/views app/viewsこれでエラーが出た場合は、gemfileに下記の2文を追加してbundle installします。
gem 'slim-rails' gem 'html2slim'bundle installこれでviewフォルダに元からあったerbファイルを削除するそうです。
※拡張子が.erbのファイルがない場合は、飛ばしてよし。bundle exec erb2slim app/views app/views -d今後自動的にslimファイルを作成されるようにするためには 、
config/application.rbにあるconfigを以下のようにslimを指定すればOK。config/application.rbmodule App class Application < Rails::Application config.generators.template_engine = :slim #slimに変更 end endslimファイルを表示させる前の準備
rails g controller tweetsviewフォルダのなかに、tweetsフォルダができています。
そのなかにindex.html.slimというファイルを新しく作ります。
そのファイルの中に分かりやすいように何か書いておきましょう。views/tweets/index.html.slimHello, world!コントローラーの中にindexアクションを追加しておきます。
controllers/tweets_controller.rbclass ArchivesController < ApplicationController def index end end次にルーティングです。
config/routes.rbRails.application.routes.draw do root "tweets#index" endサーバーを再起動して、ページを表示
サーバーの再起動を忘れないようにしましょう。
再起動しないままだと、「ArchivesController#index is missing a template for request formats: text/html」というようなエラーが出ます。参考記事
- 投稿日:2020-06-25T13:54:16+09:00
【バリデーション】rails 今日の日付以降を指定する方法
【ゴール】
日付入力の際に、今日以降の日付しか入力できないように
バリデーションをかける【メリット】
■ UXの向上
■ validate理解度向上【開発環境】
■ Mac OS catalina
■ Ruby on Rails (5.2.4.2)
■ Virtual Box:6.1
■ Vagrant: 2.2.7【実装】
※任意のメソッド名でvalidateを作成
※メソッドの処理を追記
①「return if start_day.blank?」 で空かどうか判断
②「if start_day < Date.today」で今日の日付を取得 & 条件分岐 & 比較
③「errors.add」でエラー文もついでに定義
model/item.rbvalidate :date_before_start validate :date_before_finish def date_before_start return if start_day.blank? errors.add(:start_day, "は今日以降のものを選択してください") if start_day < Date.today end def date_before_finish return if finish_day.blank? || start_day.blank? errors.add(:finish_day, "は開始日以降のものを選択してください") if finish_day < start_day end以上!!
【合わせて読みたい】
■RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い
https://qiita.com/jnchito/items/cae89ee43c30f5d6fa2c■ 【ERROR メッセージ表示】rails 部分テンプレートでいつでも使える 簡易版
https://qiita.com/tanaka-yu3/items/63b189d3f15653cae263■ 【Date.today】 今日だけの情報を取り出す方法 rails
https://qiita.com/tanaka-yu3/items/741711bd743b80eda51a
- 投稿日:2020-06-25T13:09:59+09:00
RubyのSeleniumでAttributeの値を変更したい
概要
RubyのSeleniumでスクレイピングの処理を書いているときにどうしても属性値の変更を行いたくなる場面があります。UIの振る舞いを忠実に追跡したい場合では使いませんが、UIの挙動がわかっている場合に面倒な処理を飛ばして目的のものをすぐ欲しいという場合に有効です。
方法
RubyでAttributeへのアクセスはGetAttributeがあるにも関わらずSetAttributeが見つかりません。(探し方の問題かもしれませんが)GetではDOMに変更を与えませんが書き込みでは予期せぬ振る舞いを起こす可能性から用意されていないのではと勝手に推測し、今回は「execute_script」を使用します。
execute_script.rb#ドライバーの初期化 client = Selenium::WebDriver::Remote::Http::Default.new driver = Selenium::WebDriver.for :chrome, http_client: client #<div id="mw-content-text" lang="ja" dir="ltr" class="mw-content-ltr"> #jaをenに変えたい #対象になる要素の取得 target_element = driver.find_element(:class_name, 'mw-content-ltr') driver.execute_script("arguments[0].setAttribute('lang', 'en');", target_element)参考
https://www.rubydoc.info/gems/selenium-webdriver/0.0.28/Selenium%2FWebDriver%2FDriver:execute_script
- 投稿日:2020-06-25T10:40:33+09:00
Railsで投稿編集時に予め所属カテゴリにチェックをつける方法
多対多の関係のPostとCategoryをもつテーブルに置いて、Post編集時に既存のカテゴリに予めチェックボックスをつける方法。
form_with model: @post
の形式を使わないと{checked:}
オプションを使えないので少しハマった。前提:多対多のテーブル構成を実現する
以下を参考にテーブル構成してる前提です。ただし投稿の
title
は本記事ではname
になってます。collection_check_boxesの引数に
{checked:}
オプションを追加前段の多対多のテーブルを作成している前提だと、
@post.category_ids
で所属カテゴリを配列で取得できるらしい。それに.map(&:to_param)
してやることでチェックが付く。この&:to_param
というのはいつも固定で、@post
やcategory_ids
のようにテーブルやモデルの名称によって変わるものではない。app/views/posts/edit.html.erb<%= f.label :category, 'カテゴリ' %> <%= f.collection_check_boxes(:category_ids, Category.all, :id, :name, { checked: @post.category_ids.map(&:to_param) }) do |category| %> <%= category.label do %> <%= category.check_box %> <%= category.text %> <% end %> <% end %>もし
form_with model: @post
で「undefined method 'post_path'」とエラーが出る時
form_with model: @post, url: posts_create_path do
みたいな書き方をしていると{checked:}
オプションは使えない。以下の回答のコメント欄にあるようにresouces
を使わないといけない。
https://teratail.com/questions/200474本題:最終的なコード
routes.rbRails.application.routes.draw do resources :posts, only: [:new, :create, :edit, :update] endposts_controller.rbclass PostsController < ApplicationController def new @post = Post.new end def create @post = Post.create(post_params) redirect_to('/posts/index') end def show @post = Post.find(params[:id]) end def edit @post = Post.find(params[:id]) end def update @post = Post.find(params[:id]).update(post_params) redirect_to("/posts/index") end private def post_params params.require(:post).permit(:name, category_ids: []) end endapp/views/posts/edit.html.erb<div class="main posts-index"> <div class="container"> <%= form_with model: @post do |f| %> <p> <%= f.label :name, 'name'%> <%= f.text_field :name %> </p> <p> <%= f.label :category, 'カテゴリ' %> <%= f.collection_check_boxes(:category_ids, Category.all, :id, :name, { checked: @post.category_ids.map(&:to_param) }) do |category| %> <%= category.label do %> <%= category.check_box %> <%= category.text %> <% end %> <% end %> </p> <%= f.submit '変更を保存' %> <% end %> </div> </div>その他
Railsはマイグレーションファイルの生成や
db:migrate
を少しでもミスると、特に本番デプロイ時にDBがおかしくなったりするから気をつけてね。一度でもdb:migrate
したら、もうそのマイグレーションファイルは使えないし、schemaを直接書き換えたりしてもダメだよ。こちらも参考になりそう?
【Rails】form_with/form_forについて【入門】