- 投稿日:2021-03-15T23:59:14+09:00
[Rails] 直打ち禁止方法
直打ち禁止とは?
instagram風アプリなどの作成時に、投稿者のみ、編集や削除ができるようにすることです。
current_user
などでview
のみを制御してもURLに直接https:///XXX/edit
などを打ち込むと投稿者以外のアカウントでも編集できてしまいます。これを出来なくすることを直打ち禁止といいます。前提
railsで
devise
を導入していること。実装方法
application_controller
に、before_actionメゾットを追記します。application_controllerbefore_action :configure_permitted_parameters, if: :devise_controller?次に、直打ち禁止させたいcontrollerページに以下を追記します。
今回はpost_controller.rb
という名前のcontroller
ページに追記していきます。post_controller.rbbefore_action :authenticate_user! before_action :ensure_correct_user, { only: [:edit, :update, :destroy] }
authenticate_user!
はログインしているユーザーのみ使えるようにするメゾットです。(devise
で使えるようになるヘルパーメソッド)
before_action :ensure_correct_user
を追記し、{ only: [:XXX] }
に直打ち禁止したいアクションを追記します。今回はedit
,update
,destroy
を追記しました。また、同じcontrollerページ(今回は
post_controller.rb
)のprivate
より上に下記のコードを追記します。post_controller.rbdef ensure_correct_user @post = Post.find_by(id: params[:id]) return unless @post.user_id != current_user.id redirect_to posts_path end投稿したユーザーとログインしているユーザーのidが違うとき、
posts_path
にリダイレクトするようになります。
- 投稿日:2021-03-15T23:46:22+09:00
【コードリーディング】delete_allでupdateやdeleteを実行するコードはどこなのか
はじめに
以下モデルを実装し、サンプルデータを詰めて
delete_all
メソッドを実行した際、deleteクエリではなく、updateクエリが実行されました。実装したモデル:
user.rbclass User < ApplicationRecord has_many :microposts endmicropost.rbclass Micropost < ApplicationRecord belongs_to :user endサンプルデータ:
user
、user.microposts
にそれぞれサンプルデータが入っています。> user => #<User id: 1, name: "test_user", email: "test@email.com", created_at: "2021-03-09 13:39:20.444759000 +0000", updated_at: "2021-03-09 13:39:20.444759000 +0000"> > user.microposts Micropost Load (1.0ms) SELECT `microposts`.* FROM `microposts` WHERE `microposts`.`user_id` = 1 /* loading for inspect */ LIMIT 11 => #<ActiveRecord::Associations::CollectionProxy [#<Micropost id: 1, content: "test content", user_id: 1, created_at: "2021-03-09 13:42:16.405694000 +0000", updated_at: "2021-03-09 13:42:16.405694000 +0000">]>
user.microposts.delete_all
の実行結果:> user.microposts.delete_all Micropost Update All (3.6ms) UPDATE `microposts` SET `microposts`.`user_id` = NULL WHERE `microposts`.`user_id` = 1 => 1このupdateクエリがどの条件のときにどんなコードで実行されるのか、またdeleteのときはどうなるのか気になったため、調べてみました。
環境
Ruby: 3.0.0
Ruby on Rails: 6.1.3
コード確認日: 2020/03/08delete_all実行時のクラスを把握
user.microposts
のクラスを調べてみると、CollectionProxy
になっていました。> user.microposts.class => Micropost::ActiveRecord_Associations_CollectionProxyそのため
CollectionProxy
クラスでdelete_all
メソッドが実行されているようです。仕様を読む
CollectionProxy
クラスのdelete_all
メソッドの仕様を確認します。
Deletes all the records from the collection according to the strategy specified by the :dependent option. If no :dependent option is given, then it will follow the default strategy.
For has_many :through associations, the default deletion strategy is :delete_all.
For has_many associations, the default deletion strategy is :nullify. This sets the foreign keys to NULL.
dependent
オプションの指定内容によって挙動が決まるようです。もしdependent
オプションがなければ、associationの形式によって挙動が異なるようです。
今回はdependent
オプションがなく、has_many
のassociationだけなので、デフォルトの挙動であるnullify(外部キーがNULLになる)となります。コードを読む
実際のコードとbinding.pryを使用して、コードを読んでいきます。
まずはCollectionProxy
から読んでいきます。collection_proxy.rbdef delete_all(dependent = nil) @association.delete_all(dependent).tap { reset_scope } end
@association
は、User
モデルで関連付けしているhas_many
があるので、HasManyAssociation
となります。binding.pryでの確認結果:
> @association => #<ActiveRecord::Associations::HasManyAssociation:0x00007fa9578df970 ・・・そのため、
HasManyAssociation
クラスのdelete_all
メソッドを呼び出します。
HasManyAssociation
クラス自体ではdelete_all
メソッドが定義されていないので、親クラスのCollectionAssociation
クラスを見ていきます。collection_association.rbdef delete_all(dependent = nil) if dependent && ![:nullify, :delete_all].include?(dependent) raise ArgumentError, "Valid values are :nullify or :delete_all" end dependent = if dependent dependent elsif options[:dependent] == :destroy :delete_all else options[:dependent] end delete_or_nullify_all_records(dependent).tap do reset loaded! end end
引数の
dependent
が存在し、:nullify
や:delete_all
を含んでいなければ例外を返しますが、今回は引数のdependent
を設定していないので次に進みます。次の処理では、
delete_or_nullify_all_record
メソッドに渡すdependent
をセットしています。ここでのoptions[:dependent]
は、以下のようなHogeモデルのdependent
の:delete_all
にあたります。hoge.rbclass Hoge < ApplicationRecord has_many :foos, dependent: :delete_all end今回は
has_many
にdependent
オプションがないので、dependent
はnil
となります。
最後にdelete_or_nullify_all_record
メソッドを呼びだします。has_many_association.rbdef delete_or_nullify_all_records(method) count = delete_count(method, scope) update_counter(-count) count end
scope
が新しく出てきたので、その定義を見てみます。collection_association.rbdef scope scope = super scope.none! if null_scope? scope end
親クラスの
scope
を取得しているので、その定義を見ます。association.rbdef scope if (scope = klass.current_scope) && scope.try(:proxy_association) == self scope.spawn else target_scope.merge!(association_scope) end end
klass
をbinding.pryで確認するとMicropost
になっており、klass.current_scope
を確認するとnil
になっています。binding.pryの実行結果:
> klass => Micropost(id: integer, content: string, user_id: integer, created_at: datetime, updated_at: datetime) > klass.current_scope => nilそのため、
target_scope
メソッドを見ていきます。association.rbdef target_scope AssociationRelation.create(klass, self).merge!(klass.scope_for_association) end
target_scope
ではAssociationRelation
クラスをnewしているので、scope
にはAssociationRelation
クラスのインスタンスがセットされます。collection_association.rbの
scope
メソッドに戻り、null_scope?
メソッドを見ていきます。collection_association.rbdef null_scope? owner.new_record? && !foreign_key_present? end
今回は新規レコードではなく、外部キーを持っているのでfalseが返されます。
binding.pryの実行結果:
> owner => #<User:0x00007ffe81de2fb0 id: 1, ・・・ > owner.new_record? => falseそのため、
scope
はAssociationRelation
クラスのインスタンスのままとなります。ではhas_many_association.rbの
delete_or_nullify_all_records
メソッドに戻り、delete_count
メソッドを見ていきます。has_many_association.rbdef delete_count(method, scope) if method == :delete_all scope.delete_all else scope.update_all(nullified_owner_attributes) end end
ここで
method
が、つまりはdependentが:delete_all
であるかどうかで、deleteかupdateかに分かれるようです。今回
method
はnil
なのでupdateとなるのですが、deleteクエリの実行も確認したいため、それぞれのルートを確認します。updateのルート
scope.update_all(nullified_owner_attributes)
が実行されるので、まずはnullified_owner_attributes
メソッドを見ていきます。foreign_association.rbdef nullified_owner_attributes Hash.new.tap do |attrs| attrs[reflection.foreign_key] = nil attrs[reflection.type] = nil if reflection.type.present? end end
このメソッドで
Micropost
の外部キー(user_id
)をnil
として持つHashを作成します。
nullified_owner_attributesメソッドの確認が終わったので、scope
のupdate_all
メソッドを見ていきます。relation.rbdef update_all(updates) raise ArgumentError, "Empty list of attributes to change" if updates.blank? if eager_loading? relation = apply_join_dependency return relation.update_all(updates) end stmt = Arel::UpdateManager.new stmt.table(arel.join_sources.empty? ? table : arel.source) stmt.key = table[primary_key] stmt.take(arel.limit) stmt.offset(arel.offset) stmt.order(*arel.orders) stmt.wheres = arel.constraints if updates.is_a?(Hash) if klass.locking_enabled? && !updates.key?(klass.locking_column) && !updates.key?(klass.locking_column.to_sym) attr = table[klass.locking_column] updates[attr.name] = _increment_attribute(attr) end stmt.set _substitute_values(updates) else stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name)) end @klass.connection.update stmt, "#{@klass} Update All" end
このメソッドでは3つの大きな括りができているようなので、順番に見ていきます。
if eager_loading? relation = apply_join_dependency return relation.update_all(updates) endこの
eager_loading?
メソッドは以下で定義されています。relation.rbdef eager_loading? @should_eager_load ||= eager_load_values.any? || includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?) end
eager_loadやincludeを行っていれば、trueとして返されるようです。今回は使用していないのでfalseが返されます。次は
Arel::UpdateManager.new
を使用した処理になります。stmt = Arel::UpdateManager.new stmt.table(arel.join_sources.empty? ? table : arel.source) stmt.key = table[primary_key] stmt.take(arel.limit) stmt.offset(arel.offset) stmt.order(*arel.orders) stmt.wheres = arel.constraints if updates.is_a?(Hash) if klass.locking_enabled? && !updates.key?(klass.locking_column) && !updates.key?(klass.locking_column.to_sym) attr = table[klass.locking_column] updates[attr.name] = _increment_attribute(attr) end stmt.set _substitute_values(updates) else stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name)) endここで、実行するクエリの条件をセットしています。
引数で渡されたupdates
はHashのためif文に入ります。locking_enabled?
は楽観的ロックをしているかどうかの判定ですが、今回は行っていないため、_substitute_values
メソッドの実行結果をセットします。
_substitute_values
メソッドは以下のようになっています。relation.rbdef _substitute_values(values) values.map do |name, value| attr = table[name] unless Arel.arel_node?(value) type = klass.type_for_attribute(attr.name) value = predicate_builder.build_bind_attribute(attr.name, type.cast(value)) end [attr, value] end endこのメソッドでは
value
の中身をチェックして、属性と値をセットとして持たせています。
stmt
に条件をセットし終えたので、update
メソッドを実行します。@klass.connection.update stmt, "#{@klass} Update All"
@klass.connection
をbinding.pryで確認するとMysql2Adapter
になっています。binding.pryの実行結果:
> @klass.connection => #<ActiveRecord::ConnectionAdapters::Mysql2Adapter:0x00007fd889132748 ・・・
Mysql2Adapter
クラス自体にはupdate
メソッドがないため、親クラスをたどるとDatabaseStatements
にあることが分かります。database_statements.rbdef update(arel, name = nil, binds = []) sql, binds = to_sql_and_binds(arel, binds) exec_update(sql, name, binds) end
このupdateメソッドで、
to_sql_and_binds
メソッドを使用してupdateのSQLを作り、実行となります。deleteのルート
updateの確認が終わったので、次はdeleteのルートを確認します。
has_many_association.rbdef delete_count(method, scope) if method == :delete_all scope.delete_all else scope.update_all(nullified_owner_attributes) end end
scope.delete_all
の実行となるので、delete_all
メソッドを確認します。relation.rbdef delete_all invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| value = @values[method] method == :distinct ? value : value&.any? end if invalid_methods.any? raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") end if eager_loading? relation = apply_join_dependency return relation.delete_all end stmt = Arel::DeleteManager.new stmt.from(arel.join_sources.empty? ? table : arel.source) stmt.key = table[primary_key] stmt.take(arel.limit) stmt.offset(arel.offset) stmt.order(*arel.orders) stmt.wheres = arel.constraints affected = @klass.connection.delete(stmt, "#{@klass} Destroy") reset affected end最初の部分が異なりますが、あとはupdateのときとほぼ同じになります。
まずは最初の部分を見ていきます。invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| value = @values[method] method == :distinct ? value : value&.any? end if invalid_methods.any? raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") end
NVALID_METHODS_FOR_DELETE_ALL
は、同じクラスで以下のように定義されています。INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having]そのため、ここでは
distinct
やgroup
などが含まれていれば、delete_all
メソッドを実行せず例外を投げる処理となります。今回は含まれていないため、スキップします。
その後のeager_loading?
メソッドやstmt
の条件セット処理はupdateとほぼ同じのため、スキップします。最後に
@klass.connection.delete
でdelete
メソッドを実行しています。
@klass.connection
はupdateの時と同じくMysql2Adapter
であり、delete
メソッドはupdate
メソッドの定義と同じDatabaseStatements
にあります。database_statements.rbdef delete(arel, name = nil, binds = []) sql, binds = to_sql_and_binds(arel, binds) exec_delete(sql, name, binds) endこちらも
to_sql_and_binds
メソッドで、deleteのSQLを作り、実行となります。おわりに
コードリーディングを行うことで、ドキュメントだけでなく、どこのコードでdeleteとupdateが分岐しているのか知ることができました。
コードの中身を知っておくと、あのコードが動いているのだなとイメージできて良いと感じました。
この記事が誰かのお役に立てれば幸いです。
- 投稿日:2021-03-15T23:31:13+09:00
Devise 複数のモデルで同時にログイン出来ないようにする方法
はじめに
例としてECサイトなどで、deviseをつかってUserモデルとStoreモデルを作成した場合、デフォルトではそれぞれのモデルで同時にログインや新規登録が可能です。今回はそれを修正したいと思います。
コントローラーを表示する
$ rails generate devise:controllers users rails generate devise:controllers adminsルーティング
config/routes.rbdevise_for :users, controllers: { sessions: 'users/sessions', registrations: 'users/registrations'} devise_for :stores, controllers: { sessions: 'stores/sessions', registrations: 'stores/registrations'}これを忘れるとコントローラー内でオーバーライドした内容が反映されないので注意しましょう。
モジュールの作成
./controllers/concerns/accessible.rbmodule Accessible extend ActiveSupport::Concern included do prepend_before_action :check_user end protected def check_user if current_store flash[:notice] = '店舗として既にログインしています。' redirect_to(current_store) and return elsif current_user flash[:notice] = 'ユーザーとして既にログインしています。' redirect_to(root_path) and return end end endcheck_userメソッドではそれぞれのモデルでログインしている場合はリダイレクトする処理を行っています。
これをコールバックに登録するのですが、理由は後述しますがbefore_actionではなくprepend_before_actionに登録しています。モジュールの読み込み
./controllers/users/registration_controller.rbclass Users::RegistrationsController < Devise::RegistrationsController include Accessible skip_before_action :check_user, except: [:new, :create] . . . endここでは、自分のアカウント情報にアクセスして編集する目的と、edit,update,destroyアクションはdeviseのデフォルトの機能によりログインしているアカウントしかアクセス出来ないという理由でchek_userメソッドを呼び出していません。
./controllers/users/sessions_controller.rbclass Stores::SessionsController < Devise::SessionsController include Accessible skip_before_action :check_user, only: :destroy . . . endログアウトする必要があるのでdestroyアクションには適用していません。
これで完成です。before_actionではなくprepend_before_actionにコールバック関数を登録している理由
devise/app/controllers/devise/sessions_controller.rb
より、. . prepend_before_action :require_no_authentication, only: [:new, :create] prepend_before_action :allow_params_authentication!, only: :create . . . # POST /resource/sign_in def create self.resource = warden.authenticate!(auth_options) set_flash_message!(:notice, :signed_in) sign_in(resource_name, resource) yield resource if block_given? respond_with resource, location: after_sign_in_path_for(resource) endcreateアクションにはprepend_before_actionが登録されており、それらのメソッドが実行されると
current_userがtrueを返してしまいます。よってモジュール内で指定したcheck_userメソッドはprepend_before_actionに登録する必要があります。参考
最後に
:require_no_authenticationメソッドや:allow_params_authentication!の中身をちゃんと理解していないので理解したいと思います。
何か間違った点や理解が怪しそうなところがあれば是非ご教授ください!
- 投稿日:2021-03-15T22:42:31+09:00
ridgepoleでfulltext indexを貼る方法
MySQLの全文検索インデックス
MySQL5.6から、
fulltext
インデックスを貼ることができるようになった。(pluginによるparserの変更は5.7から?)ridgepoleでfulltext indexを貼ると発生する問題
create_table :fuga do ... t.text :hoge ... end execute("CREATE FULLTEXT INDEX fk_hoge ON fuga(hoge) WITH PARSER ngram")この書き方だと、冪等性がなく、
2回目以降に実行すると、一度、remote_index :hoge
が行われたあとに、再度、execute
のCREATE FULLTEXT INDEX
が呼ばれてしまう。create_table
句でname
に対するindexを貼っていないための模様この状態だと、マイグレーションを走らせるたびに、重いINDEXを貼る作業が毎回行われるので、回避するべき事象となる。
対処方法
create_table :fuga do ... t.text :hoge t.index :hoge, type: :fulltext, ignore: true # これを追加 ... end execute("CREATE FULLTEXT INDEX fk_hoge ON fuga(hoge) WITH PARSER ngram") do |c| # INDEXが登録済みの場合は再実行しない rows = c.raw_connection.query("SHOW INDEX FROM fuga") rows.none? { _1[2] == 'fk_hoge' } end
ignore
を含んだ、TableDefinitionを追加し、execute
にINDEXの存在を確認するブロックを渡すことで、再実行してもOKになる。
超参考
- 投稿日:2021-03-15T21:40:12+09:00
Railsポートフォリオ作成 #9 ゲストログイン機能
こんにちは
今回はゲストログイン機能の実装を行いました。
(前回記事(#8 機能の拡充))私は、前職(ホテルの料飲部)における、コミュニケーションの課題を解決するアプリを作っています。
ゲストログイン機能の実装
に今回は取り組みました。
ポートフォリオにおいては必須の機能だと思われるこの機能ですが、正直言ってどのように実装するのかあまり見当がつきませんでした。
しかし、簡単ログイン・ゲストログイン機能の実装方法(ポートフォリオ用) という記事が良記事すぎたおかげで、そんなに難しくありませんでした。
実装方法自体は上記の記事に譲るとして、いくつか勉強になったことを備忘録として残したいと思います。
勉強になったこと
find_or_create_by!
DBから当てはまるものを探し出し、無い場合は作ってくれる
SecureRandom
ランダムな値を作ってくれる
パスワードの生成に利用こういった新しいメソッドなどを利用しながら、新しいロジックに触れるのは楽しいです。
まだまだ自分で複雑なロジックを組み立てることはできませんが、少しずつ線になっていく日を願って点を増やしていきたいと思います。
個人的に意識していること
実際に登録されそうな情報を登録して、実際に使われていたらこんな風だという雰囲気がわかるようにすることを意識しながら、ポートフォリオ作りをしようと思っています。
そのため、今回のゲストユーザー情報もその点を意識して登録しました。
今後も雰囲気が伝わるようなデータを登録していきたいと思います。
これで、スタッフとゲストの人数を管理する機能をつければ、最低限の機能はつけ終わることになります。
その後は、UIの向上をメインに、AWS、dockerに再挑戦していきたいと思います。
- 投稿日:2021-03-15T21:29:55+09:00
投稿に失敗して画面遷移されても入力された値が消えないようにするには
自己紹介を記述し内容をDBに保存するプログラムを作成していて空白箇所がある場合はDBに保存されずに同じpathに遷移されて、入力された内容は消えないようにするはずが全部消えてしまってどこのコードがいけないのか悩んでいて解決したので記事に出そうと思います。
以下コード
def new
introdune = Introduce.new()
enddef create
introduce = Introduce.new(introduce_params)
if introduce.save
ridirect_to root_path
else
render :new最初はこのようなコードを書いていたのですがこれだと条件分岐でfalseになった際にnewに遷移されるのですがここで遷移されるnewではまだ何も記述していない状態なので実質入力した値が消えたような現象に陥っていました。
どうすれば良いか考えた結果、elseとrender :newの間にintroduce = Introduce.new(introduce_params)ともう一度記述することによって値が消えることなく画面遷移が行われました。
- 投稿日:2021-03-15T21:04:34+09:00
【RubyonRails】no such tableのエラー
【RubyonRails】no such tableのエラー
Devise導入時にこんなエラーが、
terminal>rails db:migrate == 20210315111515 AddDeviseToUsers: migrating ================================= -- change_table(:users) rails aborted! StandardError: An error has occurred, this and all later migrations canceled: SQLite3::SQLException: no such table: users: ALTER TABLE "users" ADD "email" varchar DEFAULT '' NOT NULL C:/Users/sample/db/migrate/20210315111515_add_devise_to_users.rb:7:in `block in up' bin/rails:4:in `<main>' Caused by: ActiveRecord::StatementInvalid: SQLite3::SQLException: no such table: users: ALTER TABLE "users" ADD "email" varchar DEFAULT '' NOT NULL C:/Users/sample/db/migrate/20210315111515_add_devise_to_users.rb:7:in `block in up' C:/Users/sample/db/migrate/20210315111515_add_devise_to_users.rb:5:in `up' Caused by: SQLite3::SQLException: no such table: users C:/Users/sample/db/migrate/20210315111515_add_devise_to_users.rb:7:in `block in up' C:/Users/sample/db/migrate/20210315111515_add_devise_to_users.rb:5:in `up' bin/rails:4:in `<main>' Tasks: TOP => db:migrate (See full trace by running task with --trace)usersのテーブルが見つかりませんってことっぽい
でも確認したらsersテーブルあるんだよなぁなのでもう一度
terminal>rails g devise user >rails db:migrateを実行。
治りませんでした。なんでだろうと思い新しくプロジェクトを作成しrails db:migrateをするとそちらではできました
何が違うのかなとfileをみていたらエラーが起こる方は20210315111515_add_devise_to_users.rbclass AddDeviseToUsers < ActiveRecord::Migration[5.2] def self.up change_table :users do |t|と記述してあり
エラーが起こらない方は20210315115722_devise_create_users.rbclass DeviseCreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t|と記述してあり、change_tableをcreate_tableに直したら実行できました!!!!
このエラーに結構苦戦したため備忘録として残しておきます
- 投稿日:2021-03-15T20:50:01+09:00
【Rails】toggleとtoggle!の使い方
toggleとtoggle!
toggle(:hoge)とtoggle!(:hoge)の大きな違いは「データベースにセーブされるかどうか」と「戻り値」の違いになります。
・toggle:インスタンスに保存されているbooleanの値を反転する(データベースには変更を反映しない)。処理成功時に指定のbooleanを反転させたインスタンス自身をreturnしてくれます。(selfを返す)
・toggle!:インスタンスに保存されているbooleanの値を反転させて、データベースに保存してくれます。処理成功時にtrueをreturnしてくれます。
toggle(:hoge)でデータベースに値を保存したい場合はsaveメソッドを使えば保存できます。
使用例
以下、toggle(:activated)の使用例です。
togglerails c #データベースからデータを取得。インスタンスのactivatedの値がtrueである user = User.first >User id: 1, name: "Example User", email: "example@railstutorial.org" activated: true user.activate >true #userインスタンスのacticatedの値がfalseになる user.toggle(:activated) >User id: 1, name: "Example User", email: "example@railstutorial.org" activated: false #再度データベースから代入しなおしたら、user.acticatedがtrueのまま代入される user = User.first >User id: 1, name: "Example User", email: "example@railstutorial.org" activated: true以下、toggle!(:activated)の使用例です。(update_at等、一部割愛しています)
toggle!rails c #データベースからデータを取得。インスタンスのacticatedの値がtrueである user = User.first >User id: 1, name: "Example User", email: "example@railstutorial.org" activated: true user.activate >true #userインスタンスのacticatedの値がfalseになる user.toggle!(:activated) (0.1ms) begin transaction User Update (4.1ms) UPDATE "users" SET "updated_at" = ?, "activated" = ? WHERE "users"."id" = ? ["activated", 0], ["id", 1] (5.7ms) commit transaction >User id: 1, name: "Example User", email: "example@railstutorial.org" activated: false #再度データベースから代入しなおしたら、user.acticatedがfalseで代入される(つまり、データベースに変更が反映されている) user = User.first >User id: 1, name: "Example User", email: "example@railstutorial.org" activated: false参考
Rails の toggle と toggle! の違い
https://www.eiji56.com/2017/06/toggle/
- 投稿日:2021-03-15T19:45:17+09:00
System specを動かすのにはまった
概要
rspecでSystem specを流そうと思いましたが、いろいろハマるところが多かったので、自分がハマったところを記載しておきます。
環境
- 共通
- Ruby 2.5.8
- Rails 5.2.4
- rspec 3.10.0
- selenium-webdriver 3.142.7
- ローカル環境
- Ubuntu 16.04(WSL2)
- Chromium 87.0.4280.66
- ChromeDriver 87.0.4280.66
- CI環境
- Github Actions
Chrome環境は特にインストールしなくても大丈夫でした
ローカル環境でChromeが動かない
最初はWSL1上でSystem specを動かそうとしていましたが、うまく動いてくれませんでした。
いろいろな可能性を試したところWSLをVersion2にすることで動作させることが出来ました。
最終的に動作した内容が下記になります。
ChromeとChromeDriverのバージョンを合わせないといけないという記述がありましたが、下記をインストールすればChromiumがブラウザとして使われてバージョンも同じになるので簡単そうです。
chromeドライバのインストールsudo apt-get install chromium-chromedriverrails_helper.rbCapybara.default_driver = :selenium_chrome_headless Capybara.register_driver :selenium_chrome_headless do |app| options = Selenium::WebDriver::Chrome::Options.new options.add_argument('--headless') options.add_argument('--no-sandbox') options.add_argument('--disable-gpu') options.add_argument('--window-size=1280,1024') Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) endspec_helper.rbconfig.before(:each, type: :system) do driven_by :selenium_chrome_headless endGithub Actionsで日本語環境を整える
ローカル環境でSystem specが動くように整えた状態でGithub Actionsで動かしてみたところ、rspec自体は何もしなくて動きました
![]()
Chrome環境を整える必要があると思っていたのですが、特に何もしなくても動きました。ただ、System specは失敗になっていて失敗時の画像を見るとChromeの言語が英語になっているようでした。
user_spec.rbrequire 'rails_helper' RSpec.feature 'Users', type: :system do let!(:user) { create :user } scenario "ログインできること" do visit login_path fill_in "email", with: user.email fill_in "password", with: user.password click_button "ログイン" expect(page).to have_content("ホーム") end endChromeDriverの設定に
lang
をオプションとして追加すると解決するというページもちらほらあったのですが、うまくいきませんでした。rails_helper.rboptions.add_argument('--lang=ja-JP')結果的にはなんて言うことはなく、rspecを動かすときに
LANG
環境変数を設定してあげるだけでした
run_rspec.yml- name: Run rspec run: bundle exec rspec env: LANG: ja-JP RAILS_ENV: test最後に日本語フォントを入れてあげないと日本語が表示されないので、事前にインストールするようにしました。
run_rspec.yml- name: Install fonts run: sudo apt-get install fonts-ipafont-gothic fonts-ipafont-mincho
- 投稿日:2021-03-15T19:44:30+09:00
Ruby on Rails 新規プロジェクト作成〜bootstrapのインストールまで
①railsのバージョン確認
gem list rails②sampleフォルダを新規作成
rails _5.2.4.5_ new sample
- 投稿日:2021-03-15T19:44:30+09:00
Ruby on Rails 新規プロジェクト作成〜Bootstrapのインストールまで
①railsのバージョン確認
gem list rails②sample(名前は任意)フォルダを新規作成
rails _5.2.4.5_ new sample③Bootstrapのインストール
一. Gemfileに以下を追加gem 'bootstrap', '~> 4.1.1' gem 'jquery-rails'二. 追加したものをインストール
bundle install三. cssファイルをscssファイルに変更
mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss四. app/assets/stylesheets/application.scssの記述を全て削除して以下を追記
@import "bootstrap";五. app/assets/javascripts/application.jsに以下を追記
//= require jquery3 //= require popper //= require bootstrap-sprocketsこれでBootstrapが使える。
Bootstrapより先にgemをインストールすべきなのかもしれない。
- 投稿日:2021-03-15T19:27:38+09:00
form.collection_check_boxesを複数使ったフォーム欄の作成
form.collection_check_boxesを複数使ったフォームページを作成しようとしたらかな〜〜り苦戦したので、忘れない様に書いていこうと思います。
実現したいこと
現在問題投稿サイトを作成中です。上記画像は投稿した問題のeditページです。問題にどんなタグをつけるかをタググループ(部位、部位詳細)にそれぞれ属するタグから選んで更新でき、かつ、下記画像の様に更新したタグ(頭頸部、胸部、骨)がshowページでチェックがついた状態で表示されることが目標です。(画像のUIはPFの作成途中なので非常に見苦しいですが、ご容赦ください...)
開発環境
Mac OS Catalina: 10.15.7
ruby: 2.6.6
rails: 6.1.1モデル
今回登場するモデルは以下の3つです。(中間テーブルを除く)
Quiz
Tag
TagGroupそれぞれの関係性は
Quiz 多対多 Tag
Quiz 多対多 TagGroup
TagGroup 一対多 Tagになっています。
何故複数のform.collection_check_boxesを複数使ったフォーム欄の作成が複雑なのか
理由は
・複数のinputタグに対して同じ属性は使えない
・仮想カラムを設定しないとeditページを開いた時にチェックボックスにチェックが入った状態にならない以上二つが挙げられます。
複数のform.collection_check_boxesの実装
まず、ダメな実装方法を示します。以下の様にviewを設定します
_form.html.erb<div class="bg-info"> <label for="body-regions">部位</label> <div id="body-regions"> <%= form.collection_check_boxes :tag_ids, @body_regions, :id, :name %> </div> <label for="body-region-details">部位詳細</label> <div id="body-region-details"> <%= form.collection_check_boxes :tag_ids, @body_region_details, :id, :name %> </div> </div>
form.collection_check_boxes
で設定している第一引数をみて欲しいのですが、どちらのinputタグも:tag_ids
となっています。上述した様に、複数のinputタグに対して同じ属性は使えません。ここが一緒だと更新時のParamaterに値が入らなかったり、ストロングパラメータに弾かれたりします。*
form.collection_check_boxes
の引数などを詳しく知りたい方はこちらの記事が参考になると思います。
http://l-light-note.hatenablog.com/entry/2017/10/16/153717そこで、第一引数のメソッドを自分で作ります。第一引数のメソッドは更新対象のオブジェクトのメソッドであるため、今回だとQuizモデルで設定します。
ただし、ここでも注意点があります。Quizモデルの設定です。またまたダメな例を書きます。
model/quiz.rbhas_many :details_ids, class_name: 'Tag'_form.html.erb<label for="body-regions">部位</label> <div id="body-regions"> <%= form.collection_check_boxes :tag_ids, @body_regions, :id, :name %> </div> <label for="body-region-details">部位詳細</label> <div id="body-region-details"> <%= form.collection_check_boxes :details_ids, @body_region_details, :id, :name %> </div>二つ目のinputタグの第一引数をmodelで設定しました。上記の様に設定すると更新はできる様になりますが、editページを開いた時にチェックボックスにチェックが入った状態になりません。(原因は最後までよくわかりませんでした...)
そこで仮想カラムを作ります。そうするとそのカラム名をメソッドの様に呼び出すことができる様になり、上記の問題も解決できました。以下にその設定を書きます。(仮想カラムの設定については以下の記事を参考にしました。)
http://mainichiaisatu.hatenablog.com/entry/2017/03/26/225336model/quiz.rbattr_writer :body_region_tag_ids, :body_region_detail_tag_ids #タググループ(部位)のタグを取得 def body_region_tag_ids @body_region_tag_ids = self.tags.where(tag_group_id: 1).ids.sort if self.tag_ids.present? end #タググループ(部位詳細)のタグを取得 def body_region_detail_tag_ids @body_region_detail_tag_ids = self.tags.where(tag_group_id: 2).ids.sort if self.tag_ids.present? end_form.html.erb<label for="body-regions">部位</label> <div id="body-regions"> <%= form.collection_check_boxes :body_region_tag_ids, @body_regions, :id, :name %> </div> <label for="body-region-details">部位詳細</label> <div id="body-region-details"> <%= form.collection_check_boxes :body_region_detail_tag_ids, @body_region_details, :id, :name %> </div>controllers/quizzes_controller.rbdef update if @quiz.update(quiz_params) && @quiz.update(tag_ids: tag_params) flash[:success] = '問題を更新しました。' redirect_to @quiz else flash[:danger] = '問題は更新されませんでした' render :edit end end private def tag_params params.require(:quiz).permit(body_region_tag_ids: [], body_region_detail_tag_ids: []) params[:quiz][:body_region_tag_ids] + params[:quiz][:body_region_detail_tag_ids] end(1)まずはモデルから設定します。
attr_writer :body_region_tag_ids, :body_region_detail_tag_ids
で仮想カラム(インスタンスメソッド)を指定します。
その後、それぞれのインスタンスメソッドの処理を書きます。今回はタググループに属するタグのidを配列で取得したかったため、上記の様に書きました。(2)ビューにて第一引数を(1)で設定したインスタンスメソッドに書き換えます。
(3)quizzes#updateにて更新処理を書きます。また、ストロングパラメータにてインスタンスメソッドで取得した複数の配列を一つの配列にまとめて取得できる様にします。この配列を使って更新します。
これで実装完了です。バリデーションなどはまだ書けていませんが、忘れないうちに書かせていただきました。
結語
簡単だと思っていましたが、想像以上にかなーり手こずりました。挫けずPF作成を続けていこうと思います。
- 投稿日:2021-03-15T18:22:48+09:00
【Rails】お気に入り機能を実装しよう!
今回もレシピアプリを例にお気に入り機能を実装していきます。
完成イメージ
お気に入り機能の実装
モデルの作成
お気に入りに必要な情報は以下の通りになります。
・どの投稿をお気に入りしたかの情報(レシピ情報)
・誰がお気に入りしたかの情報(ユーザ情報)この2つの情報を保存するためにlikesテーブルを作成します。
ターミナルrails g model likes上記のコマンドで作成されたマイグレーションファイルを以下のように編集します。
references型でuserとrecipeに紐付けます。db/migrate/2021xxxxxxxxxx_create_likes.rbclass CreateLikes < ActiveRecord::Migration[6.0] def change create_table :likes do |t| t.references :user, foreign_key: true #追記 t.references :recipe, foreign_key: true #追記 t.timestamps end end endターミナルrails db:migrateこれでlikesテーブルが作成されました。
アソシエーションの設定
UserモデルとLikeモデルの関係は
・ユーザーは複数お気に入り登録することができる
・とあるお気に入り(たとえばlikesID=1)にはユーザーが一人しかいない
となるのでapp/models/user.rbclass User < ApplicationRecord has_many :recipes, dependent: :destroy has_many :likes #追記 #以下略app/models/like.rbclass Like < ApplicationRecord belongs_to :user #追記 endRecipeモデルとLikeモデルの関係は
・1つの投稿(レシピ)は複数お気に入りを持つことができる
・とあるお気に入り(たとえばlikesID=1)に紐づく投稿(レシピ)は1つしかない
となるのでapp/models/recipe.rbclass Recipe < ApplicationRecord belongs_to :user has_many :likes, dependent: :destroy #追記 #以下略app/models/like.rbclass Like < ApplicationRecord belongs_to :user belongs_to :recipe #追記 endバリデーションの設定
ユーザーが1つの投稿に対してお気に入り登録できる回数を1回にします。
つまり、user_idとrecipe_idの組み合わせが重複しないように設定します。
uniquenessヘルパーを使って重複していないか検証します。Railsガイドapp/models/like.rbclass Like < ApplicationRecord belongs_to :user belongs_to :recipe validates :user_id, uniqueness: { scope: :recipe_id } #追記 endルーティングの追加
今回、追加するルーティングはお気に入り情報を保存(create)するルーティングと削除(destroy)するルーティングになります。
また、postsに対してlikesは子の関係になるので、ネストさせどの投稿に紐づくかわかるようにします。config/routes.rbRails.application.routes.draw do #中略 #ここから編集 resources :recipes do resources :likes, only: [:create, :destroy] end #ここまで編集 endコントローラーの作成、編集
お気に入り機能に使うコントローラーを作成します。
先ほど、ルーティングでlikesと指定したのでコントローラー名をlikesとします。ターミナルrails g controller likes次にアクションを追加していきます。
親モデルに属する子モデルのインスタンスを新たに生成するのでbuildを使います。app/controllers/likes_controller.rbclass LikesController < ApplicationController before_action :authenticate_user! def create @like = current_user.likes.build(like_params) @recipe = @like.recipe if @like.valid? @like.save redirect_to recipe_path(@recipe) end end def destroy @like = Like.find(params[:id]) @recipe = @like.recipe if @like.destroy redirect_to recipe_path(@recipe) end end private def like_params params.permit(:recipe_id) end endメソッドの作成
ビューファイルを編集する前に現在サインインしているユーザーがお気に入り登録しているかどうか判断するためのメソッドを作成します。
find_byでuser_idとrecipe_idが一致するlikeを探し、なければnilを返します。
app/models/recipe.rbclass Recipe < ApplicationRecord belongs_to :user has_many :likes, dependent: :destroy #中略 #ここから追加 def liked_by(user) Like.find_by(user_id: user.id, recipe_id: id) end #ここまで追加 endビューファイルの編集
current_user != @recipe.userで自分の投稿には表示しないようにし、条件分岐に先ほど作成したliked_byメソッドを使い表示させるボタンを変化させます。
また、link_toメソッドはデフォルトではgetなのでmethod:を指定します。
app/views/recipes/show.html.erb#中略 <div class="recipe-name"> <%= @recipe.title %> </div> <div> 投稿者: <%= @recipe.user.name %>さん </div> <div class="recipe-content"> カテゴリー: <span class="recipe-category"><%= @recipe.category.name %></span> 所要時間: <span class="recipe-time"><%= @recipe.time_required.name %></span> </div> #ここから追加 <% if current_user != @recipe.user %> <div class="recipe-like"> <% if @recipe.liked_by(current_user).blank? %> <%= link_to "お気に入りに保存", recipe_likes_path(@recipe), method: :post, class: "btn btn-success btn-sm" %> <% else %> <%= link_to "保存済み", recipe_like_path(@recipe,@recipe.liked_by(current_user)), method: :delete, class: "btn btn-outline-success btn-sm" %> <% end %> </div> <% end %> #ここまで追加 #以下略以上でお気に入り登録機能が実装できました。
挙動を確認してみましょう。
お気に入りに登録したレシピを表示させよう!
ユーザーマイページにお気に入りに登録したレシピを表示させます。
ルーティングの設定
まずは、お気に入り一覧ページのルーティングの設定をしてます。
config/routes.rbRails.application.routes.draw do #中略 #ここから編集 resources :users do member do get :like end end #ここまで編集 #以下略コントローラーの編集
whereメソッドでlikesテーブルから自分のidが登録されているレコードを取得し、pluckメソッドで取得したレコードからrecipe_idを配列の形で取得します。
app/controllers/users_controller.rbclass UsersController < ApplicationController #中略 #ここから追加 def like @user = User.find(params[:id]) likes = Like.where(user_id: current_user.id).pluck(:recipe_id) @likes = Recipe.find(likes) end #ここまで追加 endビューファイルの作成、編集
まずはビューファイルを作成します。
ターミナルtouch app/views/users/like.html.erbビューファイルを以下のように編集します。
app/views/users/like.html.erb<div class="like-index text-center"> <div class="user-name"> <%= @user.name %>さんのお気に入り </div> <% if @likes.length != 0 %> <% @likes.each do |recipe| %> <div class="recipe-contents d-flex"> <% if recipe.images.present? %> <%= image_tag recipe.images[0], class: "index-img" %> <% else %> <%= image_tag "no_image.png", class: "index-img" %> <% end %> <div class="recipe"> <div class="recipe-title"> <%= link_to recipe.title, recipe_path(recipe) %> </div> <div class="recipe-content"> カテゴリー: <span class="recipe-category"><%= recipe.category.name %></span> 所要時間: <span class="recipe-time"><%= recipe.time_required.name %></span> </div> </div> </div> <% end %> <% else %> お気に入りはまだ登録されていません <% end %> </div>if文でお気に入り登録を1つもしていない場合は「お気に入りはまだ登録されていません」と表示させるようにしています。
それでは実際に表示してみましょう。お気に入り登録していない場合
お気に入り登録している場合
以上で完成です。
- 投稿日:2021-03-15T16:15:54+09:00
Ruby と Ruby on Rails の違い
この記事の目的
Railsを学んでいく過程で、純粋なRubyとの間にどのような違いがあるのかを、気づいた時にその都度アウトプットすることで理解を深めていくことを目指します。
メソッド編
・Rubyでは中身が空のまま定義されたメソッドは何も実行しません。しかしRailsでは中身が空のまま定義されたメソッドでも何らかの振る舞いをする場合があります。例えば、「AplicationController」クラスを継承したクラスの中のメソッドで静的ページのビューを出力することのみを目的とするメソッドの場合は、その静的ページのURLへのルーティングがなされ、対応するビューのファイルが作成されてさえいれば特に中身がないアクションでも動作します。
モジュール編
・Rubyでは作成したモジュールを使う際にincludeを使って明示的に読み込む必要がありますが、Railsではその必要はなく、特に何もしなくてもモジュール内で定義したメソッドを使うことができます。
- 投稿日:2021-03-15T15:58:35+09:00
gem banken を飼ってみる
gem banken を導入します。
以下のURLを優しくしただけの記事です(下記のほうが詳しい)。https://github.com/kyuden/banken
https://github.com/kyuden/banken/wiki/Tutorial-(japanese)Gemfilegem "banken"app/controllers/application_controller.rbclass ApplicationController < ActionController::Base include Banken protect_from_forgery end$ bundle install $ rails g banken:install
app/loyalties/
が作成されます。
準備OKです。$ rails g banken:loyalty posts create app/loyalties/posts_loyalty.rbさきほど作成された
ApplicationLoyalty
クラスを継承するPostsLoyalty
クラスがapp/loyalties/
配下に作成されましたapp/loyalties/posts_loyalty.rbclass PostsLoyalty < ApplicationLoyalty def update? user.admin? || record.unpublished? end endapp/controllers/posts_controller.rbclass PostsController < ApplicationController # 他の処理は省略 def update authorize! @post if @post.update(post_params) redirect_to @post, notice: 'Post was successfully updated.' else render :edit end end # 他の処理は省略 end更新処理の実行前に
Banken
が提供するauthorize!
メソッドを呼ぶことで以降の処理が実行可能かどうかを判定することができます。authorize!
は以下の処理を実行します。
PostsController
と同名のPostsLoyalty
クラスのインスタンスを作成する
PostsLoyalty
クラスのインスタンスのupdate?
メソッドを実行する
Banken::NotAuthorizedError
のエラーがでたら成功です!
- 投稿日:2021-03-15T15:19:23+09:00
windowsでrails環境構築で参考にした記事まとめ
- 投稿日:2021-03-15T13:56:47+09:00
Strong Parameters
はじめに
Strong Parametersに少しつまずいたので、今後のためにメモ。
Strong Parametersとは、リクエストパラメータを受け取る際に、想定通りのパラメータかをチェックするもの。
params.require(:user).permit(:name, :email)このように使われます。
意味合い
params.require(:user)この部分で、受け取るパラメータを指定し、
.permit(:name, :email)この部分で、パラメータから取り出しを許可するカラムを指定しています。
まとめ
上述の例では、Userモデルから取り出せるのは、名前とメールアドレスということになり、それ以外は取り出せないようにすることで、セキュリティを担保しています。
想定外の属性を、登録・更新させたくない場合に使用します。
- 投稿日:2021-03-15T12:20:22+09:00
Docker環境構築後にVS Codeで「The git repository at 'Appディレクトリ' has too many active changes, only a subset of Git features will be enabled.」に見舞われる
環境
Ruby on Rails 6.0.3
ruby 2.6.3
MacOS Catalina 10.15.7困ったこと
ローカルでVS CodeでDockerのコンテナ作りをしていたら、VS CodeのメニューバーのSource Control(編集したファイルの変更前後を表示してくれるナイスなやつ)が
5k+
になって、そのうちgitが壊れました。それからというもの画面右下に下記メッセージが表示されるようになりました。
The git repository at 'Appディレクトリ' has too many active changes, only a subset of Git features will be enabled.
(意訳:gitリポジトリの変更が多すぎるから、一部のgit機能しか使えなくなってるよ。)
そして、左のメニューバーの編集したファイルの差異を示してくれる箇所が時計マークでワークしなくなり、左下にgitのブランチ名を表示してくれなくなりました。。。VS codeを再起動しても変わらずです。
実はこの問題に見舞われるのは2度目なのですが、1度目どう解決したかメモしておらず(LaravelのプロジェクトをGitHubにアップするのに試行錯誤した話)、ネットにもあまり情報がなく困り果てており、もうこのアプリではVS Codeのこの機能は諦めて全てターミナルでgit statusして凌ぐか...と思っていた矢先、弊社の最強先輩陣が助けてくれました。
解決
vendor/bundleを.gitignoreに含めてコミットしたら直りました!
このディレクトリはbundleをinstallする場所。
Dockerの環境構築した時に、5k以上のファイルがここにインストールされてしまったのかなと推察しております。too many changes(変更が多すぎる)ということで怒られているので、.gitignoreが適切に設定されているかを疑うのがいいのかもしれません。
- 投稿日:2021-03-15T11:39:42+09:00
Dockerとdocker-composeを使ったRails APIモードの環境構築
Dockerとdocker-composeを使い、Rails6をAPIモードで動かす環境を構築しました。最後に動作確認もしています。
前提
Docker & docker-composeをインストール済み
動作環境
- macOS Catalina 10.15.7
- Ruby 2.7.1
- Ruby on Rails 6.0.3
- Docker 20.10.2
- docker-compose 1.27.4
ディレクトリを作成します。
mkdir rails_api_docker cd rails_api_docker
GemfileとDockerfileを作成します。
Gemfilesource 'https://rubygems.org' gem 'rails', '6.0.3'DockerfileFROM ruby:2.7.1-alpine3.11 ENV BUNDLER_VERSION=2.1.4 WORKDIR /usr/src/app COPY Gemfile . RUN apk update && \ apk add --no-cache \ linux-headers \ libxml2-dev \ curl-dev \ make \ gcc \ libc-dev \ g++ \ sqlite-dev \ tzdata && \ gem install bundler && \ bundle install COPY . .
Dockerfileを元にDockerイメージを作成します。
docker build -t rails_api:6.0.3 .
作成したイメージからコンテナを起動し、Railsアプリケーションを作成します。
docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app rails_api:6.0.3 rails new . --skip-keeps -M -C -S -J -B
docker-composeでコンテナを起動します。
docker-compose up
localhost:3000
にアクセスします。起動画面が表示されれば成功です。
APIの動作確認
起動中のコンテナにログインします。
docker-compose exec api sh
scaffold
を使ってcontrollerとmodelを作成します。rails g scaffold User name:string
/db/seeds.rb
を以下を追記してテストデータを作成します。seeds.rbUser.create name: "Euclid"
DBを初期化し、テストデータを反映させます。
rails db:create rails db:migrate rails db:seed
/app/controllers/users_controller.rb
のindexアクションを以下に変更します。users_controller.rbdef index @users = User.all render json: @users end
localhost:3000/users
にアクセスして以下が表示されれば成功です。
読んでいただきありがとうございました!ご指摘やご意見などありましたらコメントしていただけると嬉しいです?