20211011のRailsに関する記事は15件です。

devise ログイン後の遷移先を指定する時に気になったことを深掘りしてみた。

はじめに deviseについて学習していたところ、ログイン後の遷移先を指定先についてのメソッドの挙動について気になったことがありましたので、詳しく調べてみました。 ログイン後の遷移先の指定方法 指定方法はいたって簡単で、application_controller.rbにafter_sign_in_path_for(resource)メソッドを定義するだけです。 app/controllers/application_controller.rb class ApplicationController < ActionController::Base def after_sign_in_path_for(resource) pages_show_path end end 上記の例の場合、ログイン後にルーティングで定義されているpages_show_pathに遷移するようになります。 どうしてこのメソッドでログイン後の遷移先を指定できるのか Deviseのgemをinstallすると、ApplicationControllerにユーザー認証用の基本的なメソッドを付与します。 つまり、after_sign_in_path_forメソッドをApplicationControllerに再定義することで、メソッドのオーバーライドをしているのです。 参考:Deviseのモヤモヤを解消して快適なRailsライフを送ろう! で、何が気になったのか 何が気になったのかというと、after_sign_in_path_forメソッドのデフォルトの挙動です。 deviseについて学習するために参考にさせていただいたQiitaの記事では、以下のように記載がありました。 ログインすると、デフォルトでは root_url に飛ばされます。 Devise側で定義されている、after_sign_in_path_forメソッドを確認したら、遷移先がroot_pathになるような処理がされているんだろうなと思い、実際に確認してみたところ、、、 lib/devise/controllers/helpers.rb def after_sign_in_path_for(resource_or_scope) stored_location_for(resource_or_scope) || signed_in_root_path(resource_or_scope) end ん?なんか思っていたのと違う。。。 メソッドの説明を確認してみます。 By default, it first tries to find a valid resource_return_to key in the session, then it fallbacks to resource_root_path, otherwise it uses the root path (デフォルトでは、セッション内の有効なresource_return_to keyキーを探します。resource_return_to keyキーが見つからなかった場合は、resource_root_pathにフォールバックし、それも見つからなかった場合は、root pathを使います。) 引用:Devise::Controllers::Helpers#after_sign_in_path_for どうやら、デフォルトでは、すぐにroot_pathを使うのではなく、まず、sessionの情報を使って遷移先を決定するようです。 説明文の感じだと、sessionの中身は、URLの情報が入っていそうですね。 ということで、デフォルトの挙動をコードベースで確認してみたくなったので調べてみました。 コードベースで確認してみる コードベースでも確認してみます。 lib/devise/controllers/helpers.rb def after_sign_in_path_for(resource_or_scope) stored_location_for(resource_or_scope) || signed_in_root_path(resource_or_scope) end ||は、左から順に評価し、一番最初に"true"になったものを返す演算子です。 つまり、処理はこんな感じです。 まず、stored_location_for(resource_or_scope)メソッドを実行。ここで実行結果が"true"であれば、処理終了。 実行結果が"false"だった場合、signed_in_root_path(resource_or_scope)を実行。 それでは、各メソッドの処理を確認していきましょう。 stored_location_for(resource_or_scope)メソッドについて 1.stored_location_for(resource_or_scope)メソッドの概要 まずコードを確認してみましょう。 lib/devise/controllers/store_location.rb # File 'lib/devise/controllers/store_location.rb', line 18 def stored_location_for(resource_or_scope) session_key = stored_location_key_for(resource_or_scope) if is_navigational_format? session.delete(session_key) else session[session_key] end end 処理としては、 session_keyという変数にstored_location_key_for(resource_or_scope)メソッドの戻り値を代入しています。 次に、is_navigational_format?の結果がtrue or falseで以下のように条件分岐をしています。 trueの場合:session[session_key]の情報を削除。 falseの場合:session[session_key]を戻り値として返す。 メソッドの説明を確認してみます。 Returns and delete (if it's navigational format) the url stored in the session for the given scope. Useful for giving redirect backs after sign up: (引数として与えられたスコープにかかるsessionに保存された、urlを返します。もし、それがナビゲーションのフォーマットであれば、削除します。これは、サインアップ後のリダイレクトバック先を指定するのに便利です。) 引用:Devise::Controllers::StoreLocation#stored_location_for sessionに保存されている情報がナビゲーションの情報であった場合は、sessionを削除。 sessionに保存されている情報がurlであった場合は、urlの情報を返す。 やはり、sessionに保存されている情報は、urlでしたね。 次に、sessionについて確認しましょう。 2.変数session_keyの値について 変数session_keyは、stored_location_key_for(resource_or_scope)メソッドの戻り値が代入されています。 それでは、stored_location_key_for(resource_or_scope)メソッドについて確認します。 devise/lib/devise/controllers/store_location.rb def stored_location_key_for(resource_or_scope) scope = Devise::Mapping.find_scope!(resource_or_scope) "#{scope}_return_to" メソッドは、resourceもしくは、scopeを引数にしています。今回は、Userモデルにdeviseを実装しているので、引数にはuserが入ります。 変数scopeには、DeviseのMappingクラスのfind_scope!メソッドの戻り値が代入されます。 最終的に文字列"#{scope}_return_to"を返します。 変数scopeの値について 変数scopeの値を理解するためには、以下の2つを理解する必要があります。 Devise::Mapping find_scope!(resource_or_scope) 1.DeviseのMappingクラスは、routes.rb内のdevise_forで設定された各リソースを元にマッピングオブジェクトを作成します。この時、マッピングオブジェクトの名前は、単数形の名詞になります。 ex.devise_for :users → user Class: Devise::Mapping 2.find_scope!(resource_or_scope)メソッドは、deviseのscopeをシンボルの形式で返してくれるメソッドです。scopeが見つからなかった場合は、エラーを吐きます。 devise/lib/devise/mapping.rb # Receives an object and find a scope for it. If a scope cannot be found, # raises an error. If a symbol is given, it's considered to be the scope. def self.find_scope!(obj) obj = obj.devise_scope if obj.respond_to?(:devise_scope) case obj when String, Symbol return obj.to_sym when Class Devise.mappings.each_value { |m| return m.name if obj <= m.to } else Devise.mappings.each_value { |m| return m.name if obj.is_a?(m.to) } end raise "Could not find a valid mapping for #{obj.inspect}" end 内部的には、devise_scopeメソッドで作成されたdeviseのscopeをシンボルの形式で返しています。 この2つ結果を踏まえると、最終的な戻り値は、 :user_return_to となります。 つまり、変数session_keyの値は、:user_return_toであり、 session[:user_return_to]となります。 sessionのキーとするためにシンボルにして値を返していたのですね。 session[:user_return_to]の値について ここまでで、session[:user_return_to]に、urlの情報が保存されていて、ログイン後にこのurlに遷移するところまでわかりました。 次に生じる疑問点は、sessionではどのurlを保存しているのかです。 1.store_location_for(resource_or_scope, location)メソッドについて stored_location_for(resource_or_scope)メソッドのすぐ下に以下のようなメソッドが定義されています。 devise/lib/devise/controllers/store_location.rb # Stores the provided location to redirect the user after signing in. # Useful in combination with the `stored_location_for` helper. # # Example: # # store_location_for(:user, dashboard_path) # redirect_to user_facebook_omniauth_authorize_path # def store_location_for(resource_or_scope, location) session_key = stored_location_key_for(resource_or_scope) path = extract_path_from_location(location) session[session_key] = path if path end Stores the provided location to redirect the user after signing in. Useful in combination with the stored_location_for helper. (ユーザーをサインイン後にリダイレクトさせるために引数で提供された場所を保存します。stored_location_for ヘルパーと一緒に使うと便利です。) ログイン後の遷移先の情報を保存してくれるメソッドのようです。 処理を確認すると、、、、 pathという変数にextract_path_from_location(location)の戻り値が代入されています。そしてその変数pathが存在する場合にpathの値をsession[session_key]に代入しています。 つまり、extract_path_from_location(location)の戻り値こそが、ログイン後のリダイレクト先ということになります。 それでは、extract_path_from_location(location)メソッドについて確認しましょう。 def extract_path_from_location(location) uri = parse_uri(location) if uri path = remove_domain_from_uri(uri) path = add_fragment_back_to_path(uri, path) path end end 処理を確認すると、 引数の値をparse_uriメソッドでuriとして生成し、変数uriに代入します。 変数uriの値が存在する場合、pathとして機能するように変数の値を加工しています。 ここでふと思いました、『あれ、でも引数には何が入るんだろう』と。 現時点では、アプリケーション側では、store_location_for(resource_or_scope, location)メソッドを定義していませんので、引数を設定することはありません。 『ということは、store_location_for(resource_or_scope, location)メソッドをアプリケーション側で定義する必要があるのか。』 ということで、もう少し調べてみることにしました。 調べてみると、deviseのwikiに以下の記事がありました。 How To: Redirect back to current page after sign in, sign out, sign up, update なるほど、やはり自分で定義してあげないといけないのか。と思った矢先に以下の記述に目が止まりました。 The following guides are already implemented in Devise 4.7 version(以下のガイダンスについては、Devise4.7バージョンで実装済みです。) 実装済み??ってことは、自分でメソッドを定義しなくても使えるってことなんでしょう。 2.store_location!メソッドについて githubには便利な機能があって、コード間を楽々移動できちゃいます。この機能を使ってstore_location_for(resource_or_scope, location)メソッドを参照しているメソッドを探します。すると、devise/lib/devise/failure_app.rbの243列目に参照しているメソッドがありました。 failure_app.rbは、ユーザーが認証に失敗した際の処理を記述したファイルです。 メソッドの説明を確認します。 devise/lib/devise/failure_app.rb # Stores requested URI to redirect the user after signing in. We can't use # the scoped session provided by warden here, since the user is not # authenticated yet, but we still need to store the URI based on scope, so # different scopes would never use the same URI to redirect. def store_location! store_location_for(scope, attempted_path) if request.get? && !http_auth? end Stores requested URI to redirect the user after signing in. We can't use the scoped session provided by warden here, since the user is not authenticated yet, but we still need to store the URI based on scope, so different scopes would never use the same URI to redirect.(サインイン後にユーザーをリダイレクトさせるためのリクエストされたURIを保存します。ここでは、wardenで提供されたscoped sessionを使用することはできません。なぜなら、ユーザーは、まだ認証されていないからです。しかし、異なるscopeが同じURIをリダイレクト先として使用しないようにするために、scopeに基づいたURIを保存しておく必要があります。) 処理としては、 request.get?の結果が"true"でかつhttp_auth?の結果が"false"であった場合にstore_location_for(scope, attempted_path)を実行するというものです。 1.メソッドの制御について request.get?は、リクエストがHTTP GETメソッドであれば"true"を返すメソッドです。 http_auth?は、ajaxによるリクエストであった場合に"true"を返すメソッドです。 devise/lib/devise/failure_app.rb # Choose whether we should respond in an HTTP authentication fashion, # including 401 and optional headers. # # This method allows the user to explicitly disable HTTP authentication # on AJAX requests in case they want to redirect on failures instead of # handling the errors on their own. This is useful in case your AJAX API # is the same as your public API and uses a format like JSON (so you # cannot mark JSON as a navigational format). def http_auth? if request.xhr? Devise.http_authenticatable_on_xhr else !(request_format && is_navigational_format?) end end このメソッドの制御については、deviseのwikiには以下のようにstorable_location?メソッドとして定義されていました。 private # Its important that the location is NOT stored if: # - The request method is not GET (non idempotent) # - The request is handled by a Devise controller such as Devise::SessionsController as that could cause an # infinite redirect loop. # - The request is an Ajax request as this can lead to very unexpected behaviour. def storable_location? request.get? && is_navigational_format? && !devise_controller? && !request.xhr? end def store_user_location! # :user is the scope we are authenticating store_location_for(:user, request.fullpath) end end この説明書きによると以下の場合には、ロケーション情報が保存されないとのことです。 リクエストがGETではないとき(常に同じ結果を返す(冪等)ものでないリクエスト) リクエストがDeviseのコントローラーによって制御されているもの(無限リダイレクトループを引き起こす) リクエストがAJAXによるものである場合(全く予期せぬ動作を引き起こす) おそらく、store_location!メソッドのメソッド制御の部分がこれにあたるのでしょう。 2.store_location_forの引数について メソッドの実行部分をもう一度見てみましょう。 store_location_for(scope, attempted_path) 引数には、以下の二つが設定されています。 scope attempted_path ●引数scopeについて scopeは、以下のように定義されています。 devise/lib/devise/failure_app.rb def scope @scope ||= warden_options[:scope] || Devise.default_scope end 処理としては、インスタンス変数scopeの値が存在しない場合、warden_options[:scope]を自己代入。それも無い場合は、Devise.default_scopeを自己代入するようです。 1. warden_options[:scope] wardenとは、rubyで作成された、ウェブアプリケーションに認証メカニズムを提供するために設計されたRackベースのミドルうウェアです。 Deviseは、wardenベースで作られたものです。 それでは、warden_optionsの定義を確認してみましょう。 devise/lib/devise/failure_app.rb def warden_options request.respond_to?(:get_header) ? request.get_header("warden.options") : request.env["warden.options"] end メソッドは三項演算子の形式になっています。 respond_to?メソッドで、request(レシーバー)にget_headerメソッドが定義されているかを確認し、定義されていれば、request.get_header("warden.options")を実行し、レシーバーに定義されていなかった場合は、request.env["warden.options"]を実行します。 次に、get_headerメソッドについて確認します。 lib/rack/request.rb # Get a request specific value for `name`. def get_header(name) @env[name] end メソッドは、rackで定義されています。 リクエストを引数で与えられた名前のついた特定の値にするメソッドのようです。 request.env["warden.options"]と結局同じになるようですね。 つまり、このメソッドは、 "warden.options"という名前で保存されたリクエストの情報を返すメソッドのようです。 このリクエストは、認証失敗時にユーザーが行なったリクエストのことです。 ここまでのことを踏まえると、Deviseは、未ログインユーザーが認証失敗時にリクエストした情報を、env["warden.options"]として保存していることがわかります。 長くなりましたが、warden_options[:scope]には、現在devise認証を用いているモデルのuserが入ることになります。 2. Devise.default_scopeは、 routes.rbで一番最初に定義されたdevise_for :以下の部分がデフォルト値で、config/initializers/divise.rbで設定している場合は、それを使用します。今回は、userモデルのみしかdeviseを実装していませんので、userとなります。 2. warden_options[:attempted_path] ここには、認証失敗時にユーザーがアクセスを試みたパスの情報が入っています。 devise/lib/devise/failure_app.rb def attempted_path warden_options[:attempted_path] end 参考:Deviseちょっとしたtips2つ 引数についてまとめます。 1. scope=Deviseでログイン失敗時に操作しようとしたモデル名(単数形) 2. attempted_path=ログイン失敗時にアクセスを試みたパス devise/lib/devise/failure_app.rb def store_location! store_location_for(scope, attempted_path) if request.get? && !http_auth? end rubyメソッドの引数は、値渡しです。 scopeとattempted_pathの定義された値がそのまま引数として渡されます。 Deviseでの認証失時、store_location_for(scope, attempted_path) では、以下のことが実行されます。 session[:user_return_to]=認証失敗時のパス情報 after_sign_in_path_forメソッドのデフォルト挙動(まとめ) lib/devise/controllers/helpers.rb def after_sign_in_path_for(resource_or_scope) stored_location_for(resource_or_scope) || signed_in_root_path(resource_or_scope) end after_sign_in_path_for(resource_or_scope)は、ログイン前に、認証に失敗しているか否かによって、ログイン後の遷移先が変わります。 具体的には、以下のようになります。 1. ログイン前に認証に失敗して、ログインページにリダイレクト後、ログインをした場合 →リダイレクト直前のページへ遷移。 2. ログインページからログインした場合 →リソースベースのルートページを定義していれば、そこへ。定義していなければ、ルートページへ遷移。 after_sign_in_path_forメソッドのデフォルトは、基本的には、ルートパスへの遷移です。しかし、ログインが必要なページ(仮にAページとします)に未ログイン状態でアクセスした場合については、ログインした後にAページに遷移させるということがわかりました。ちなみにこれをフレンドリーフォワーディングといいます。 参考:フレンドリーフォワーディング おわりに とてもシンプルに見えたコードでしたが、追っていくとなかなか複雑でした。 実際、コードリーディングをしてみましたが、知らないこと、わからなことばかりで大変な部分もありましたが、初学者の身からすると大変勉強になったと感じました。間違っている部分がございましたらご指摘いただけると幸いです。 参考 heartcombo/devise Rails ガイド How To: Redirect back to current page after sign in, sign out, sign up, update warden wiki [Rails] deviseの使い方(rails6版) Deviseのモヤモヤを解消して快適なRailsライフを送ろう! Deviseちょっとしたtips2つ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseでユーザー機能実装済みのアプリに管理者機能を追加

やまだゆう(@yamaday0u)です。 今回は、Railsのdevise gemでログイン認証機能(ユーザー管理機能)を実装済みのアプリに、管理者機能を追加する実装例をご紹介します。 目標 この記事では、 deviseでログイン認証機能を実装済みのアプリに 新たなテーブルを追加せずに 管理者機能を実装すること を目標にします。 管理者とユーザーのテーブルを別々に作りたい場合は以下のブログ記事を参考にしてください。 【Rails】deviseを使用して管理者と会員を作成する方法(初心者向け)(Nulfasのブログ) 前提 devise gemですでにユーザー管理機能を実装済み = ユーザーを管理するusersテーブルが存在していること。 usersテーブルには最低限の以下のカラムが存在しています。 カラム名 データ型 name string email string encrypted_password string 実装のポイント 既存のusersテーブルにboolean型のadminカラムを追加して、adminカラムの値がtrueのユーザーを管理者とします。 手順 migrationファイルを作成 既存のusersテーブルにデータ型がboolaen型のadminカラムを追加するためにmigrationファイルを作成します。 ターミナル rails g migration add_admin_to_users admin:boolean migrationファイルの編集&実行 作成したmigrationファイルに以下の通り記述します。 ここでのポイントは、初期値をdefault: falseで指定しているところです。 これによりusersテーブルの既存のレコードにも初期値がfalseの状態でadminカラムが追加されます。 db/migrate/2021XXXXXXXXXX_add_admin_to_users.rb class AddAdminToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :admin, :boolean, default: false end end ターミナルでマイグレートします。 ターミナル rails db:migrate 管理者ユーザーを作成 管理者ユーザー(のレコード)をseeds.rbファイルを用いて作成します。 ここでのポイントはもちろん、adminカラムの値をtrueにしていることです。 後の工程で、adminカラムの値がtrueであるか否かを利用してログイン後のマイページの表示を切り替えます。 また、ぼくの場合、githubにアプリのソースコードを公開しているので、.envというgemを使って、メールアドレスやパスワードは環境変数を定義して指定しました。 db/seeds.rb User.create!( name: "管理者", email: ENV['ADMIN_EMAIL'], password: ENV['ADMIN_PASSWORD'], password_confirmation: ENV['ADMIN_PASSWORD'], admin: true ) seedsファイルに記述した内容を実行します。 ターミナル rails db:seed これで、管理者ユーザーが作成、usersテーブルに登録されました。 Routeの定義 ぼくのポートフォリオアプリであるGroup Calendarを例にrouteの定義を説明します。 通常のユーザーであれば、ログイン後にconfig/routes.rbに定義したresources :calendarsによりマイページ(indexアクション)にリダイレクトします。 そこで管理者ユーザーの場合は、namespaceを利用して以下のように記述し、admin/calendarsをマイページのpathにして、管理者ページであることを明示的にしました。 config/routes.rb namespace :admin do # 以下の記述により、admin/calendarsへのpathが開かれます。 resources :calendars, only: %i[index] end Controllerの作成その1 routeの定義でadmin/calendarsというpathを開いたので、以下のようにAdmin::Calendars Controllerを作成します。 ターミナル rails g controller admin/calendars 作成したcontrollerに以下の通り記述します。 app/controllers/admin/calendars_controller.rb class Admin::CalendarsController < ApplicationController def index # 省略 end end Controllerの作成その2 管理者ユーザーにはニュースリリース機能を使えるようにしたいので、News Controllerを作成します。 ターミナル rails g controller news 作成したNews Controllerに以下のように記述します。 private actionかつbefore_actionのcheck_admin?により、管理者以外のユーザーが管理者用のページであるニュースリリース関連機能のページを表示できないようにしています。 app/controllers/news_controller.rb class NewsController < ApplicationController before_action :check_admin? def index # 省略 end # 中略 private def check_admin? unless current_user.admin redirect_to root_path end end end View Viewはこのように書きます。 ログイン中のユーザー(current_user)のadminカラムの値がtrueなら、ニュースリリース機能関連のページへのリンクを表示します。 app/views/news/_link_to_news_release_page.html.erb <% if current_user.admin? %> <li><%= link_to "News Release", news_index_path %></li> <% end %> 完成!! 以上でdeviseでログイン認証機能を実装済みのアプリに新たなテーブルを追加せずに管理者機能を実装する手順が完了しました。 ぼくのポートフォリオアプリでは、以下のように管理者ページにのみNews Release用のページリンクが表示されるようにしました。 管理者ユーザーのマイページ 一般ユーザーのマイページ Rails学習中のみなさまの参考になれば幸いです。 ポートフォリオ 未経験からのエンジニア転職に向けて作ったポートフォリオを公開しています。 よろしければ覗いて見てください。 Group Calendar
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails * Vue の歴史を辿ってみる

Rails * Vue の歴史を辿ってみる この話を読み終えてわかること Rails * Vueの歴史ってまだまだ浅いこと Rails * Vueの情報はググってみると、やり方が2つに別れていること 初学者でも、全然戦えるフィールドであること Railsの歴史 1995(26年前) Ruby誕生、割と古くからある 割と泣かず飛ばずの言語だった 2005(16年前) RubyOnRailsが誕生 有名企業が次々採用 Airbnb twitter GitHub Rubyが注目され始めるタイミングが瞬間がここ Vueの歴史 2009年(12年前) 土台であるnode.js誕生 WEB業界に衝撃を与えた front, backの完全分業化の実現に大貢献 2014(7年前) Vue.js誕生 従来最強は Facebook製のReact(2011〜) Google製のAngular(2012〜) そこに一石投じた1人の開発者[Evan You]によって誕生 Railsさん、node.jsすげえに気づいて取り込み始める 2016年(5年前) webpackの拡張GEM、webpackerを本気でサポート開始 node.jsのplugin管理をRails上でやりやすくしようと言う試み開始 ただ、node.jsをそもそも使ってた人たちから酷評される webpackerは独自すぎて使いにくい その頃、Vueは 2016年(5年前) Vue2.0が発足 これが強烈に評判がよく、一気に普及 github上でreactやangularのstar数を追い抜く レスポンス早く、書きやすく、めちゃいいやんってなる RailsのWebpackerいらないトレンドが普及 2018年頃(3年前くらい) 脱webpacker記事が増える ならば、しっかりとfront, backを切り分けた方が良いでしょ。と言う流れに node.jsをRailsの中で組み立てるのではなく node.js環境とRails環境をそもそも分けてしっかり疎結合に そして最近のVueの動き 2020年(1年前) Vue3.0がスタート TypeScriptいいよね!の流れと仲良くするため?が強い印象 一層、バグ少なく組みやすいcomposition APIをリリース ここまでの話からわかること 2009年(12年前) WEBの世界は大きく変化があった。 2016年(5年前) Vue.jsの躍進が始まった 2018年(3年前) Rails * Vueのあるべき姿が見え始めた なのでこの分野で見たら、3年プレイヤーくらいが最年長と言う感じ ここまでの状況を踏まえて Rails * Vueって、歴史そんなに深くない この掛け算で戦ってる人達 まだ初学者の方でも、全然手の届く範囲で先駆者たちは走ってる さらにまだまだ進化は続くので、トレンドを追っていれば追い抜くのも難しくない またみんな1年生に戻るかもしれないので というわけで 初学者の方も、焦ったり、すごい人見てげんなりしなくても大丈夫 この分野に限ってはみんな若手 楽しんでたら、気がついたら追い越してるっていう可能性は全然あるので 焦らず楽しんでいきましょうー!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】scopeではなぜfirstメソッドを定義しないのか

きっかけ 下記のコードを見たときに少し違和感を覚えた。 class Article < ApplicationRecord scope :published, -> { where(published: true) } end class ArticlesController < ApplicationController def index @article = Article.published.first end end firstメソッドをモデルのpublishedメソッドの中で書いてしまった方が コントローラーが少しすっきりする気がするのになんでそうしてないんだろうと疑問に思ったので scopeについて改めて調べてみた。 class Article < ApplicationRecord scope :published, -> { where(published: true).first } end class ArticlesController < ApplicationController def index @article = Article.published end end 問題になりそうなケースと原因 例えば条件に合うArticleがない場合、nilが返るはずのところでなぜか全件取得されてしまうらしい。 これはscopeの中で行われている処理の結果がnilの場合、全件取得が行われる仕様になっているため。 まとめ scopeは「メソッドを定義するより1行ですっきり書けるもの」という認識しかなかったが 仕様をよく理解して適切に使っていきたい。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル第1章、herokuへデプロイする際にエラー

この記事について 現在、Railsチュートリアル6版の2週目にチャレンジ中のNIssyiと申します。 1週目に挑戦した際にも同じ箇所でつまずいていました。自分への備忘録、そして自分と同じような初心者がつまずいた際に参考にできることを目指して記事を投稿します。 開発環境 開発環境は以下のとおりです。 Macbook Pro(13-inch, M1, 2020) Mac OS Big Sur 11.6 VSCode (Visual Studio Code - Insiders 1.62.0) エラーの概要 Railsチュートリアル6版の「1.5.2Herokuにデプロイする(1)」でherokuへデプロイしようとしたときに発生したエラーです。 ターミナル $ git push heroku main Enumerating objects: 113, done. Counting objects: 100% (113/113), done. Delta compression using up to 8 threads Compressing objects: 100% (94/94), done. Writing objects: 100% (113/113), 141.99 KiB | 7.10 MiB/s, done. Total 113 (delta 7), reused 0 (delta 0), pack-reused 0 remote: Compressing source files... done. remote: Building source: remote: remote: -----> Building on the Heroku-20 stack remote: -----> Determining which buildpack to use for this app remote: ! Warning: Multiple default buildpacks reported the ability to handle this app. The first buildpack in the list below will be used. remote: Detected buildpacks: Ruby,Node.js remote: See https://devcenter.heroku.com/articles/buildpacks#buildpack-detect-order remote: -----> Ruby app detected remote: -----> Installing bundler 2.2.21 remote: -----> Removing BUNDLED WITH version in the Gemfile.lock remote: -----> Compiling Ruby/Rails remote: -----> Using Ruby version: ruby-2.7.4 remote: -----> Installing dependencies using bundler 2.2.21 remote: Running: BUNDLE_WITHOUT='development:test' BUNDLE_PATH=vendor/bundle BUNDLE_BIN=vendor/bundle/bin BUNDLE_DEPLOYMENT=1 bundle install -j4 remote: Your bundle only supports platforms ["arm64-darwin-20"] but your local platform remote: is x86_64-linux. Add the current platform to the lockfile with `bundle lock remote: --add-platform x86_64-linux` and try again. remote: Bundler Output: Your bundle only supports platforms ["arm64-darwin-20"] but your local platform remote: is x86_64-linux. Add the current platform to the lockfile with `bundle lock remote: --add-platform x86_64-linux` and try again. remote: remote: ! remote: ! Failed to install gems via Bundler. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed remote: ! remote: ! ## Warning - The same version of this code has already been built: f0b18d1d98d71b106e8873195b323b18f6077dbf remote: ! remote: ! We have detected that you have triggered a build from source code with version f0b18d1d98d71b106e8873195b323b18f6077dbf remote: ! at least twice. One common cause of this behavior is attempting to deploy code from a different branch. remote: ! remote: ! If you are developing on a branch and deploying via git you must run: remote: ! remote: ! git push heroku <branchname>:main remote: ! remote: ! This article goes into details on the behavior: remote: ! https://devcenter.heroku.com/articles/duplicate-build-version remote: remote: Verifying deploy... remote: remote: ! Push rejected to xxxxxxxxxxxxxxxxxx. remote: To https://git.heroku.com/xxxxxxxxxxxxxxxxxx.git ! [remote rejected] main -> main (pre-receive hook declined) error: failed to push some refs to 'https://git.heroku.com/xxxxxxxxxxxxxxxxxx.git' 基本的にチュートリアルに沿って進めているのになぜかエラーが発生しました。エラーの原因を特定してみようと思います。 エラーが発生していそうな箇所を探す 先ほどのログからエラーの情報が書いてありそうなところを探してみます。 ログの最初の方に、「!」が出ているところがありますね。 ターミナル $git push heroku main Enumerating objects: 113, done. (省略) remote: -----> Determining which buildpack to use for this app remote: ! Warning: Multiple default buildpacks reported the ability to handle this app. The first buildpack in the list below will be used. remote: Detected buildpacks: Ruby,Node.js "Warning"と警告を発しているのはわかるんですが…英語力がないのでそれ以降はなんて書いてあるのかサッパリです。こんなときはGoogle先生に翻訳してもらいましょう! google翻訳 警告:複数のデフォルトビルドパックが、このアプリを処理する機能を報告しました。 以下のリストの最初のビルドパックが使用されます。 検出されたビルドパック:Ruby、Node.js ビルドパックとやらが競合を起こしたのでしょうか…? ただ、ここでは勝手に解決されているようなので、デプロイで発生したエラーの原因ではなさそうです。 引き続き、エラーの原因を探してみます。 ターミナル remote: Running: BUNDLE_WITHOUT='development:test' BUNDLE_PATH=vendor/bundle BUNDLE_BIN=vendor/bundle/bin BUNDLE_DEPLOYMENT=1 bundle install -j4 remote: Your bundle only supports platforms ["arm64-darwin-20"] but your local platform remote: is x86_64-linux. Add the current platform to the lockfile with `bundle lock remote: --add-platform x86_64-linux` and try again. remote: Bundler Output: Your bundle only supports platforms ["arm64-darwin-20"] but your local platform remote: is x86_64-linux. Add the current platform to the lockfile with `bundle lock remote: --add-platform x86_64-linux` and try again. remote: remote: ! remote: ! Failed to install gems via Bundler. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed remote: ! (省略) ログの中盤で「!」が続いたり、「Push failed」となっている箇所を発見しました。このあたりでエラーが起きていそうな気がします。 ターミナル remote: Your bundle only supports platforms ["arm64-darwin-20"] but your local platform remote: is x86_64-linux. Add the current platform to the lockfile with `bundle lock remote: --add-platform x86_64-linux` and try again. remote: Bundler Output: Your bundle only supports platforms ["arm64-darwin-20"] but your local platform remote: is x86_64-linux. Add the current platform to the lockfile with `bundle lock remote: --add-platform x86_64-linux` and try again. 2回も似たような文章が表示されている箇所を発見しました。「try again」と文末に書いてあります。なにかをもう一度やり直せと言っているのでしょうか? google翻訳 バンドルはプラットフォーム["arm64-darwin-20"]のみをサポートしますが、ローカルプラットフォームはx86_64-linuxです。 `bundle lock --add-platform x86_64-linux`を使用して現在のプラットフォームをロックファイルに追加し、再試行してください。 ログに書いてあるとおり、Bundlerでサポートされているプラットフォームと自分のローカルプラットフォームの差異が原因のようです。 エラーの文章で検索してみる エラーらしきものを発見したものの、どのように対処すればよいのかわからないのでエラーメッセージ自体をgoogleで検索してみます。すると、参考になりそうなページを発見。こちらの【Rails】Bundler 2.2.x以降は開発者が適切なプラットフォームを追加する必要があるを参考に、以下のbundleコマンドを実行してみます。 ターミナル $ bundle lock --add-platform x86_64-linux Gemfile.lockにプラットフォームが追加されました。 Gemfile.lock PLATFORMS arm64-darwin-20 x86_64-linux エラーを解消、再びherokuへデプロイ ここで慌てずに、変更をコミットしておきます。 ターミナル $ git commit -am "プラットフォームをGemfile.lockに追加" では、デプロイしてみましょう。 ターミナル $ git push heroku Enumerating objects: 116, done. (省略) remote: https://xxxxxxxxxxxxxxxxxx.herokuapp.com/ deployed to Heroku remote: remote: Verifying deploy... done. To https://git.heroku.com/xxxxxxxxxxxxxxxxxx.git * [new branch] main -> main デプロイができたようです!! ターミナルのログに表示されている"To https://....git"のリンクを開くか、heroku openコマンドでデプロイできているか確認してみます。 ターミナル $ heroku open 無事、表示したいRailsアプリケーションのビューが表示されていたら完了です! 感想 エラーが発生した際には以下の点を気をつけてみようと思います。 エラーログを読んでみる 英語が読めないならgoogle翻訳など翻訳サービスにメッセージをぶっこんでみる エラーログ自体で検索してみる。 参考にさせていただいたページ 【Rails】Bundler 2.2.x以降は開発者が適切なプラットフォームを追加する必要がある
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails 管理者が全Userの投稿削除をする(備忘録)

はじめに 備忘録的に書いておくので、少しちがっているところもあるかもしれません? 説明も薄めです、ご了承ください。 Ruby on rails をつかって、Twitterアプリをつくっています。管理者が自分の投稿以外の、あらゆるユーザーの投稿を消せるようにしたいと思います。 ユーザー登録はGemのdeviseで実装しています。 Userに管理者を追加する ターミナル rails g migration AddAdminToUsers xxxxxxxxx_add_admin_to_users class AddAdminToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :admin, :boolean, default: false #ここを追加 end end ターミナル rails db:migrate 次にseedファイルに管理者権限を持つユーザーを追加します。 db/seeds.rb User.create!(username: "管理者", email: "admin@example.jp", password: "11111111", password_confirmation: "11111111", admin: true) ターミナル rails db:seed 管理者用のコントローラーをつくる ターミナル  rails g controller admin::tweets controller/admin/tweets_controller  class Admin::TweetsController < ApplicationController # before_action :authenticate_user!も必要なら書いてください!   before_action :if_not_admin   before_action :set_restaurant, only: [:index, :new, :create, :show, :edit, :destroy] #onlyのアクションは必要なものだけ書いてください #中略 def indexとか必要なものを書いてください!   private   def if_not_admin   redirect_to root_path unless current_user.admin?   end   def set_restaurant   @tweet = Tweet.find(params[:id])   end end Routing設定 以下のコードを追加してください ターミナル resources :tweets namespace :admin do resources :tweets, only: [:index, :new, :create, :show, :edit, :destroy] end viewの設定 好きなところにコードを追加してください app/views/tweets/index.html.erb <div class="tweets-container"> <% @tweets.each do |t| %> <div class="tweet"> <%= t.user.email %> <%= t.body %> <%= t.created_at %> <%= link_to "詳細へ", tweet_path(t.id) %> <% if user_signed_in? && current_user.id == t.user_id %> <%= link_to "編集する", edit_tweet_path(t.id) %> <%= button_to "削除する", tweet_path(t.id), method: :delete %> <% elsif user_signed_in? && current_user.admin? && current_user.id == t.user_id %> <%= link_to "編集する", edit_tweet_path(t.id) %> <%= button_to "削除する", tweet_path(t.id), method: :delete %>    <% end %>   <% if current_user.admin? %> <%= link_to "管理者が削除するよ", tweet_path(t.id), method: :delete %> <% end %> </div> <% end %> </div> おわりに わたしは以上のコードで実装しましたが、ところどころ書かなくてもいい場所もあるかもしれません? あくまでも参考例ですので、ご自身で足りないところ足りてるところを試行錯誤していただけると助かります。 間違い等もございましたらコメントで教えていただけるとうれしいです! よろしくお願いいたします 参考サイト 【初心者向け】管理者ユーザーと管理者用controllerの追加方法[Ruby, Rails] 投稿の削除を投稿者と管理者どちらからもできるようにしたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] DBの値を反映するレーダーチャートの実装(Chart.js 3.5.1, gon)

はじめに オプションの設定で意図しない動作に苦しめられた。 何に苦しめられたかを結論から話すと、最新版の3.x.x系をインストールしているのに2.x.x系のドキュメント、記事を参考に進めてしまっていたこと。 yarn経由の導入ではバージョンを指定しないと3.x.x系の最新版が手に入ってしまう。 安定板は2.x.x系なので、公式ドキュメントも多くの記事も2.x.x系で解説されおり、そちらを参考にしていたため意図しない動作に見舞われてしまった。 公式ドキュメントの[入門]→[3.x移行ガイド]を参考に実装。 結果としてバージョン3.5.1で解決できたので、その備忘録としてレーダーチャートの実装を記事にまとめる。 バージョンを意識する大切さがよくわかりました。 環境 Rails 6.1.4 chart.js 3.5.1 gon 6.4.0 スクショ 導入から完成図まで、これとそのまんま同じになる。 chart.jsの導入 yarn add chart.js yarn経由でChart.jsをインストール。 ここでバージョンを指定しないと最新版がインストールされる事になる。 安定板は app/javascript/packs/application.js import 'chart.js/dist/chart'; ひとまず導入は完了。 動作確認 rails g controller graphs index 動作確認を行うためにトップページを設定する。 config/routes.rb Rails.application.routes.draw do root to: 'graphs#index' end ルーティングを設定。 app/views/graphs/index.html.erb <canvas id="myChart" width="400" height="400"></canvas> <script> var ctx = document.getElementById('myChart').getContext('2d'); var myChart = new Chart(ctx, { type: 'bar', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [{ label: '# of Votes', data: [12, 19, 3, 5, 2, 3], backgroundColor: [ 'rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)' ], borderColor: [ 'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)' ], borderWidth: 1 }] }, options: { scales: { y: { beginAtZero: true } } } }); </script> 公式サイトにあるサンプルを貼りつけてスクショと同じようにグラフが表示されていればOK。 本題 javascriptファイルを作成 touch app/javascript/packs/graph.js 空のjsファイルを作成。 ここにサンプルで言うところの<script>、つまりグラフの部分を書いていく。 app/javascript/packs/graph.js document.addEventListener('turbolinks:load', () => { // チャートのデータ const radarLabel = ["項目1", "項目2", "項目3", "項目4", "項目5", "項目6"]; const radarData = [1, 2, 3, 4, 5, 0];      const radarChartData = { labels: radarLabel, datasets: [ { label: 'タイトル', data: radarData, backgroundColor: 'rgba(255, 99, 132, 0.2)', borderColor: 'rgba(255, 99, 132, 1)', borderWidth: 1, spanGaps: true, }, ], }; const radarChartOption = { scales: { r: { min: 0, max: 5, ticks: { stepSize: 1, callback: function (value, index, values) { if (value == 0) { return 'レベル0'; } else if (value == 1) { return 'レベル1'; } else if (value == 2) { return 'レベル2'; } else if (value == 3) { return 'レベル3'; } else if (value == 4) { return 'レベル4'; } else if (value == 5) { return 'レベル5'; } }, }, }, }, };     //チャートを表示 const radarChartContext = document.getElementById('radar-chart').getContext('2d'); new Chart(radarChartContext, { type: 'radar', data: radarChartData, options: radarChartOption, }); }); typeには、何のチャートを使うかを指定。今回はレーダーチャートの実装が目的なのでradarと記述。 それ以外のチャートタイプについては公式ドキュメントを参照。 その他の記述の解説 data 名称 説明 labels: データの項目 datasets: データのカスタマイズを指定 label: タイトル backgroundColor: 背景の色 borderColor: 線の色 borderWidth: 線の太さ spanGaps: trueならデータのない点、またはnullの点との間に線を描画を許可。 options 名称 説明 scales: 軸の設定 r: 指定する軸 min: 軸の最小値 max: 軸の最大値 ticks: 目盛りの設定 stepSize: 目盛り幅の固定サイズ callback: データラベルをポイントラベルに変換する。 2.x.x系から3.x.x系の変更では、ticksの中にmin,maxを扱わないなど仕様の変更があるため、他記事で2.x.x系を参考にしている時は注意が必要。 graph.jsファイルの読み込みを有効化 app/javascript/packs/application.js require('./graph') これがないと読み込まれないので忘れず記述する。 表示したい場所にcanvasを記述 <canvas id="radar-chart" width="400" height="400"></canvas> サンプルで貼り付けた<script>の部分はjsファイルに移動したので不要。 <canvas>を残して.getElementById('radar-chart')と同じidを設定する。 名称 説明 図形を描画 getContext() グラフィックを描画するためのメソッドやプロパティを持つオブジェクトを返す 2d 2Dグラフィックを描画するためのメソッドやプロパティを持つオブジェクトを返す。 canvasのgetContext("2d")って何 DBから値を取得する方法 gonというGemを使用。 このgonはRailsで定義した変数をJavaScriptでも使えるようにするRubyのライブラリ。 DBの値をコントローラからJSファイルに送る工程を辿らせる。 gonの導入 gemfile. gem "gon" 忘れずにbundle installでGemをインストール。 コントローラからJSファイルへデータを渡す <%= Gon::Base.render_data %> まずはgonを使用するビューファイルに上記のコードを記述。 <canvas>の上に配置する。 app/controllers/graphs_controller.rb def index gon.chart_label = ["項目1", "項目2", "項目3", "項目4", "項目5", "項目6"]; gon.chart_data = [1, 2, 3, 4, 5, 0]; end gon.をつけた変数名を定義。この変数がjacascriptにデータを渡す役割を持つ。 app/javascript/packs/graph.js // チャートのデータ const radarLabel = gon.chart_label; const radarData = gon.chart_data; 直前に定義した変数をradarLabelとradarDataに書き換える。 これでアクションからjsファイルに値を渡す流れができた。 DBの値を取得 rails g model Graph label:string data:integer graphモデルを作成。 忘れずrails db:migrateすること。 わかりやすいようにlabelとdateとした。 app/controllers/graphs_controller.rb def index gon.chart_label = Graph.pluck(:label); gon.chart_data = Graph.pluck(:data); end pluck()は引数で指定したカラムの値を配列で返すメソッド。 この記述によってGraphモデルから値を配列として受け取る準備ができた。 初期データの作成 seeds.rb list = [ { label: "項目1", data: 1 }, { label: "項目2", data: 2 }, { label: "項目3", data: 3 }, { label: "項目4", data: 4 }, { label: "項目5", data: 5 }, { label: "項目0", data: 0 } ] Graph.create!(list) rails db:seedで初期データを投稿。 サンプルと同じように表示されていれば完了。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rspec豆知識: 外側let!と内側letの同名シンボルが混在する時に、内側letは外側のタイミングで評価される

前提 rspec --version RSpec 3.10 - rspec-core 3.10.1 - rspec-expectations 3.10.1 - rspec-mocks 3.10.2 - rspec-rails 5.0.2 - rspec-support 3.10.2 問題 このテストファイルを実行した時、標準出力にはどの順番で何が出力されるでしょうか。 以下のポイントに注意してください。 一番外側の :a は let! で正格評価になっている context内の :a は let で遅延評価になっている expect(a).to be true されている この記事のタイトルのことはひとまず忘れて、純粋な気持ちで問題を解いてみてください。 sample_spec.rb RSpec.feature type: :system do let!(:a) do pp 'outer variable (let!)' true end before do pp 'outer before' end context do let(:a) do pp 'inner variable (let)' true end before do pp 'inner before' end # 実行されるテスト it do pp 'inner it' expect(a).to be true end end end 解答 "inner variable (let)" "outer before" "inner before" "inner it" 補足 外側の変数が let(:a) で、両方の変数が遅延評価になっている場合は、純粋に遅延評価され、以下のようになる。 "outer before" "inner before" "inner it" "inner variable (let)" expect(a).to be true のタイミングで a が評価されるためである。 しかし外側に let! がある場合は let! が本来評価されるタイミングで内側のcontext側の let が評価されるため、解答のような順番になる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Hamlとは?

「HTML avstraction markup langage」が正式名称です。 マークアップ言語(HTML/XML)を生成する為のマークアップ言語です。 より短いコードで綺麗に簡潔にコードを書くことができます。 Hamlの書き方 基本記法 %タグ名でタグと閉じタグが生成されます。 正しくインデントをすることが必要です。 index.html.haml %ul %li Hello World! %li hoge hoge! htmlに変換すると↓ index.html <ul> <li>Hello World!</li> <li>hoge hoge!</li> </ul> 属性をつける 属性をつける場合は、 %タグ名{属性:"値"}or%タグ名{:属性=>"値"} index.html.haml %ul %li %a{href: "https://muscle-meal-recipes.com"} Hello World! %li %a{:href => "https://muscle-meal-recipes.com"} hoge hoge! htmlに変換すると↓ index.html <ul> <li> <a href="https://muscle-meal-recipes.com">Hello World!</a> </li> <li> <a href="https://muscle-meal-recipes.com">hoge hoge!</a> </li> </ul> 省略記法 classとid よく使うclassとidについては省略することができます。 index.html.haml / class %p{class: "hogehoge"} クラスほげほげ %p.hogehoge クラスほげほげ / id %p{id: "hogehoge"} アイディーほげほげ %p#hogehoge アイディーほげほげ inde.html <p class="hogehoge">クラスほげほげ</p> <p class="hogehoge">クラスほげほげ</p> <p id="hogehoge">アイディーほげほげ</p> <p id="hogehoge">アイディーほげほげ</p> div divについても省略することができます。 index.html.haml / class %div{class: "hogehoge"} ディブほげほげ .hogehoge ディブほげほげ / id %div{id: "hogehoge"} ディブほげほげ #hogehoge ディブほげほげ inde.html <div class="hogehoge">ディブほげほげ</div> <div class="hogehoge">ディブほげほげ</div> <div id="hogehoge">ディブほげほげ</div> <div id="hogehoge">ディブほげほげ</div> 最後に 今回は、Haml記法についてご紹介しました。 間違え等あるかと思いますので、コメント等でご指摘願います。 最後まで読んでいただきありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails エンコードして、文字を含んだ検索URLを作成したい【URI.encode_www_form_component】

概要 Twitterから取得した内容をDBに保存した。 保存した複数のデータに関連したGoogle検索ページに遷移するボタンを作成したい。 条件 brand = "Supreme / Yohji Yamamoto®" price = "¥6,600-" item = "New Era Beanie Black" この3つを含む検索画面に遷移させたい。 目標の画面はこれ↓ 行き詰まったポイント googleの検索urlに文字列を組み込めない urlはこのような形になる "https://www.google.co.jp/search?q=" q=の後に検索したい文字を組み込む ここにそのまま入れ込むと... ターミナル brand = "Supreme / Yohji Yamamoto®" price = "¥6,600-" item = "New Era Beanie Black" word = "#{brand} #{price} #{item}" url = "https://www.google.co.jp/search?q=" search_url = url+word irb(main):015:0> p search_url "https://www.google.co.jp/search?q=Supreme / Yohji Yamamoto® ¥6,600- New Era Beanie Black" => "https://www.google.co.jp/search?q=Supreme / Yohji Yamamoto® ¥6,600- New Era Beanie Black" search_urlにurlが代入されたが、URLとして機能しない... こんな感じになってしまい、link_toやbutton_toに使用しても、画面に遷移してくれない!!! https://www.google.co.jp/search?q=Supreme / Yohji Yamamoto® ¥6,600- New Era Beanie Black 最終的に成功した方法 エンコードが必要 エンコードってそもそも何? URLエンコードとは、URL/URIのファイル名やクエリ文字列などの一部としては使用できない記号や文字を、使用できる文字の特殊な組み合わせによって表記する変換規則。 エンコードにしてくれる便利なものを発見 URI.encode_www_form_component 文字列を URL-encoded form data の1コンポーネントとしてエンコードした文字列を返します。 これを使用することによって文字列をエンコードしてくれ、検索できるようになる。 流れ 1.googleの検索url urlはこのような形になる "https://www.google.co.jp/search?q=" q=の後に検索したい文字を組み込む 2.情報をurlに組み込む ターミナル brand = "Supreme / Yohji Yamamoto®" price = "¥6,600-" item = "New Era Beanie Black" word = "#{brand} #{price} #{item}" # エンコードして検索に使用できる形に変換する enc = URI.encode_www_form_component(word) url = "https://www.google.co.jp/search?q=" search_url = url+enc irb(main):028:0> p search_url "https://www.google.co.jp/search?q=Supreme+%2F+Yohji+Yamamoto%C2%AE%E3%80%80%C2%A56%2C600-%E3%80%80New+Era+Beanie+Black" => "https://www.google.co.jp/search?q=Supreme+%2F+Yohji+Yamamoto%C2%AE%E3%80%80%C2%A56%2C600-%E3%80%80New+Era+Beanie+Black" https://www.google.co.jp/search?q=Supreme+%2F+Yohji+Yamamoto%C2%AE%E3%80%80%C2%A56%2C600-%E3%80%80New+Era+Beanie+Black これを検索すると、目標の画面のページ↓に遷移する。 おまけ 通常は URI.encode_www_form を使うほうがよいでしょう。 とありましたが、encode_www_formだと Enumerable(通常はネストした配列かハッシュ)を受け取ります。 らしいので、今回は使用しなかったです。 参照
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】ストロングパラメーターはなぜ必要か?paramsの脆弱性

はじめに どうも、27歳未経験からエンジニア転職を目指しているもきおです。 Railsでコントローラを記載する際に最後にストロングパラメータ書いてねって記載されてたりしますが、そもそも ストロングパラメータって何ぞや?って思いながらも当時はとりあえず動けばいっかみたいな感じであまり理解しないまま進めてしまってました。 ストロングパラメータの記載はこんな感じ user_controller.rb private def user_params params.require(:user).permit(:name,:email,:password) end 今回はなんでこれが必要なのって話とparamsは使いようによってセキュリティの脆弱性があるよってお話をしていきたいと思います。 ストロングパラメータを使わないとどうなる? ストロングパラメータを使用しないとどのようなセキュリティ上の問題が起こるのでしょうか? 例えば以下の記載でuser_controllerのcreateを作成したとします。 user_controller.rb private def create @user = User.new(params[:user]) end 一見前回のparamsの記事のようにparamsによってuserの値を全て取ってきてuserテーブルにユーザーの情報が情報が格納されるので問題ないように思えます。 ※paramsに関しては前回書いた記事をご覧いただけますと幸いです。 しかし、ユーザー情報を全て格納されるというのは 極めて危険な状態です。 セキュリティ上危険な例 実際の例を見てみましょう。User情報にadmin(管理者)属性を持たせたとしましょう。 admin=’1’という値をparams[:user]の一部に紛れ込ませて渡してしまえば、簡単に管理者属性を持たせる事ができてしまいます。これはcurlなどのコマンドを使えば簡単に実現できてしまいます。 ※Railsチュートリアル7章ユーザー登録参照 https://railstutorial.jp/chapters/sign_up?version=6.0#code-first_create_action 悪意のあるユーザが自身に管理者権限を付与するなどシステムを自由に操作できてしまう危険性があるのです。 これを防ぐためにストロングパラメータによって特定の情報しか受け取らない設定を行い、意図しない登録、更新を防ぐ事ができるのです。 実際にストロングパラメータを使用しよう ストロングパラメータをの基本形は以下になります。 user_controller.rb private def create @user = User.new(params[:user_params]) end 先程の[:user]→[:user_params]に変更しています。 これはストロングパラメータを使いやすくするために、user_paramsという外部メソッドを使うのが慣習になっています。このメソッドは適切に初期化したハッシュを返し、params[:user]の代わりとして使われます。 ストロングパラメータ記述の仕方 user_controller.rb private def user_params params.require(:キー(モデル名)).permit(:カラム名1,:カラム名2,・・・) end requireの後にモデル名、permitの後に受け取るカラム名を指定します。 これを元に最初に記述した user_controller.rb private def user_params params.require(:user).permit(:name,:email,:password) end これで受け取る情報を制限し、悪意のあるユーザーに余計な値(管理者権限等)を受け渡さないように セキュリティを強化できました。 あとがき 今回はストロングパラメータについて理解を深めていきました。 この記事が少しでも良いと感じていただけましたらLGTMポチッといただけますと幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] タグ機能

ポートフォリオにタグ機能を実装した時のメモです。 完成イメージ 投稿画面でタグが投稿でき投稿したタグで絞り込み検索ができる機能です。 実装 今回実装する部分の関係性は以下のような関係性です。 イベントモデル(CRUD機能)は既に実装済みとします。 モデル作成 モデルを作っていきます。 ターミナル $ rails g model tagmap item:references tag:references $ rails g model tag tag_name:string タグ投稿するのでtagモデルにはtag_nameが必要です。 ターミナル rails db:migrate リレーション 続いてリレーション app/models/event.rb has_many :tagmaps, dependent: :destroy has_many :tags, through: :tagmaps app/models/tag.rb has_many :tagmaps, dependent: :destroy has_many :events, through: :tagmaps thoroughを使うことで、tagmaps経由でEventモデルにアクセスできるようになってます。 app/models/tagmap.rb belongs_to :event belongs_to :tag タグ作成 タグを作成できるようにしていきます。 app/controllers/events_controller.rb def create @event = current_user.events.build(event_params) #--------------------追加した部分---------------------- if params[:event][:tag_name].present? tag_list = params[:event][:tag_name].split(nil) @event.save_event_tag(tag_list) end #---------------------------------------------------- if @event.save redirect_to event_path(@event), notice: '作成しました' else flash.now[:error] = '作成に失敗しました' render :new end end splitの引数にnilを指定することで以下の例のように文字列の先頭と末尾の空白文字を除いた上で「空白文字に一致する部分」で分割します。 例 irb(main):001:0> " あいう えお か ".split(nil) => ["あいう", "えお", "か"] 今回はこれを利用しsave_event_tagというメソッドを作ってcreateできるようにします。 app/models/event.rb def save_event_tag(tags) # 既にタグあるなら全取得 current_tags = self.tags.pluck(:tag_name) unless self.tags.nil? # 共通要素取り出し old_tags = current_tags - tags new_tags = tags - current_tags # 古いタグ削除 old_tags.each do |old_name| self.tags.delete Tag.find_by(tag_name:old_name) end # 新しいタグ作成 new_tags.each do |new_name| post_tag = Tag.find_or_create_by(tag_name:new_name) self.tags << post_tag end end タグ投稿の際に同じタグを何度も投稿しないように投稿したタグが既に存在する場合pluckメソッドを使って一度タグを全て取得し、タグの共通要素だけを取り出し、古いタグを削除し新しくタグを作成しています。 あとはタグ投稿できるフォーム部分を作れば投稿できます。 app/views/events/_form.html.haml %div = f.label :name, 'タグ' %div = f.text_field :tag_name, class: 'form-control' タグ更新 続いて投稿されたタグを更新できるようにしていきます。 app/controllers/events_controller.rb def edit @event = current_user.events.find(params[:id]) #---------------------追加部分----------------------- @tag_list = @event.tags.pluck(:tag_name).join(",") #--------------------------------------------------- end def update @event = current_user.events.find(params[:id]) #---------------------追加部分----------------------- if params[:event][:tag_name].present? tag_list = params[:event][:tag_name].split(nil) @event.save_event_tag(tag_list) end #--------------------------------------------------- if @event.update(event_params) @event.save_event_tag(tag_list) redirect_to event_path(@event), notice: '更新しました' else flash.now[:error] = '更新に失敗しました' render :edit end end edit actionでは投稿したタグをjoinで区切って表示しています。 update actionではcreateと同じ処理を書いています。 app/views/events/_form.html.haml %div = f.label :name, 'タグ' %div = f.text_field :tag_name, value: @tag_list class: 'form-control' タグ検索 最後に検索部分です。 検索部分は既にransackを導入済みであることを想定しています。 app/controllers/events_controller.rb def search @q = Event.ransack(params[:q]) @events = @q.result(distinct: true) if params[:tag_id].present? @tag = Tag.find(params[:tag_id]) @events = @tag.events.order(created_at: :desc).all end @tag_lists = Tag.all end tag_idで検索をかけています。 あとはタグ一覧として投稿したタグをHTML上で反映できるようにすれば完成です。 .tag %p.tag-title タグ一覧 - @tag_lists.each do |list| = link_to list.tag_name, events_search_path(tag_id: list.id) 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] Ajaxで作るクリップ機能(いいね機能)

ポートフォリにAjaxでいいね機能を実装した時のメモです。 Ajaxとは? JavaScriptを使ってサーバーにリクエストを投げて、レスポンスに基づきHTMLに変更を加える技術のことらしい。 分かりやすくいうとページのリロードなしで表示が切り替わるってこと。 身近なのはやっぱりTwitterのいいねですかね。 多対多のクリップ機能は既に実装済みでとします。 ちなみに今回の場合は、以下のようにUserモデルとEventモデルの中間テーブルとしてClipモデルがあることとしてます。 完成イメージ クリップしたときにリロードなしでクリップできていてクリップしたイベントとしてクリップイベント一覧に反映されている。 実装 ① jQuery導入 jQueryを導入していきます。 ターミナル yarn add jQuery yarnとはJavaScriptのライブラリを管理するもの。Rubyでいうgemのようなもの。 npmというのもあるがRailsでは基本的にyarnを使うらしい。 次にwebpackerのサーバーを立ち上げます。 ターミナル bin/webpack-dev-server webpackerとはwebpackのRails版みたいなもので複数のJavaScriptファイルを読み込んで1つのファイルに出力してくれます。 これをすることでJavaScriptの読み込みを早くすることができ開発スピードを上げることができます。 それでは動作確認をしていきます。 app/javascript/packs/application.js import $ from 'jquery' document.addEventListener('DOMContentLoaded', () => { $('.title').on('click', () => { window.alert('CLICKED') }) }) 確認方法は何でもいいんですが、今回はtitleというクラスのついた部分をクリックしたらアラートが出るようにして動作を確認していきます。 以下のようにタイトルをクリックしてアラートが出てればOKです。 ② クリップの状態を非同期で取得する まずはアクセスした時にイベントがクリップしてるのかしてないかの状態を判断するAPIを作っていきます。 クリップしているかどうかのメソッドを作って、 app/models/user.rb def has_clipped?(event) clips.exists?(event_id: event.id) end 現状のclip_controller.rbにshow actionを追加します。 config/routes.rb resource :clip, only: [:show, :create, :destroy] app/controllers/clips_controller.rb class ClipsController < ApplicationController before_action :authenticate_user! #-------------------------追加----------------------- def show event = Event.find(params[:event_id]) clip_status = current_user.has_clipped?(event) render json: { hasClipped: clip_status } end #--------------------------------------------------- def create event = Event.find(params[:event_id]) event.clips.create!(user_id: current_user.id) redirect_to event_path(event) end def destroy event = Event.find(params[:event_id]) clip = event.clips.find_by!(user_id: current_user.id) clip.destroy! redirect_to event_path(event) end end やっていることはアクセスしたイベントがクリップされているかいないかを判断しています。 クリップしているイベントには{"hasCliped":true}というデータがjsonで返されます。 このステータス状態によってHTMLを変更していきます。 axiosをインストールします。 ターミナル yarn add axios axiosでgetリクエストをしてステータス状態を取得します。 その前に、イベントのステータス状態を取得するのにはevent_idが必要なのでクリップボタンを設置するHTML部分に以下を記述することでevent_idの取得ができます。 app/views/events/show.html.haml .container#event-show{data: {event_id: @event.id}} あとは取得したevent_idを使ってgetリクエストしていきます。 app/javascript/packs/application.js import axios from 'axios' document.addEventListener('DOMContentLoaded', () => { const dataset = $('#event-show').data() const eventId = dataset.eventId axios.get(`/events/${eventId}/clip`) .then((response) => { console.log(response) const hasClipped = response.data.hasClipped }) }) axiosのレスポンスの結果次第でクリップボタンの表示をJavaScriptで切り替えできるようにしていきます。 app/views/events/show.html.haml .clip - if user_signed_in? .clip__active.hidden.active-clip クリップ中 .clip__icon.hidden.inactive-clip クリップする app/assets/stylesheets/application.scss .hidden { display: none; } クリップボタンの部分のHTMLにhiddenクラスを両方につけdisplay: none;のcssを当てます。 app/javascript/packs/application.js const handleClipDisplay = (hasClipped) => { if (hasClipped) { $('.active-clip').removeClass('hidden') } else { $('.inactive-clip').removeClass('hidden') } } axiosのレスポンスのステータス状態に合わせてhiddenクラスをつけるか外すかを決めることでクリップボタンのHTMLが変化します。 そしてこのfunctionを先程のgetリクエスト部分に記述します。 app/javascript/packs/application.js import axios from 'axios' document.addEventListener('DOMContentLoaded', () => { const dataset = $('#event-show').data() const eventId = dataset.eventId axios.get(`/events/${eventId}/clip`) .then((response) => { console.log(response) const hasClipped = response.data.hasClipped //-------追加------------------- handleClipDisplay(hasClipped) //------------------------------ }) }) これで非同期でクリップしているかしていないかを取得することができます。 ③非同期でクリップする 次は実際にクリップするpostリクエスト部分とクリップを外すdeleteリクエスト部分を作っていきます。 APIを作っていきます。 リダイレクトしていた部分をrenderできるように変更していきます。 app/controllers/clips_controller.rb class ClipsController < ApplicationController before_action :authenticate_user! def show event = Event.find(params[:event_id]) clip_status = current_user.has_clipped?(event) render json: { hasClipped: clip_status } end def create event = Event.find(params[:event_id]) event.clips.create!(user_id: current_user.id) render json: { status: 'ok' } #変更部分 end def destroy event = Event.find(params[:event_id]) clip = event.clips.find_by!(user_id: current_user.id) clip.destroy! render json: { status: 'ok' } #変更部分 end end axiosでpostリクエストを作る前にターミナルで以下をインストールしてapplication.jsに記述します。 ターミナル yarn add rails-ujs app/javascript/packs/application.js import { csrfToken } from 'rails-ujs' axios.defaults.headers.common['X-CSRF-Token'] = csrfToken() ここでやっているのはセキュリティ対策です。 postリクエストの場合はgetリクエストと違いセキュリィティ的に問題があるので上の記述がないとpostリクエストが通りません。 csrfTokenというのは鍵みたいなもので、その鍵をaxiosのときはデフォルトで持たせるように記述しています。 postリクエスト部分 app/javascript/packs/application.js $('.inactive-clip').on('click', () => { axios.post(`/events/${eventId}/clip`) .then((response) => { if (response.data.status === 'ok') { $('.active-clip').removeClass('hidden') $('.inactive-clip').addClass('hidden') } console.log(response) }) .catch((e) => { window.alert('Error') console.log(e) }) }) .then((response) => {}はリクエストがうまくいった時の処理。今回はリクエストがうまくいったらクリップした状態にボタンを変更させています。 .catch((e) => {}の部分はリクエストが失敗した時の処理。今回はwindow.alertでErrorという文字列を表示させます。 最後にクリップを外すdeleteリクエスト $('.active-clip').on('click', () => { axios.delete(`/events/${eventId}/clip`) .then((response) => { if (response.data.status === 'ok') { $('.active-clip').addClass('hidden') $('.inactive-clip').removeClass('hidden') } console.log(response) }) .catch((e) => { window.alert('Error') console.log(e) }) }) やっているのはpostの逆の処理です。 動作確認をして画面がリロードせずクリップできていたら完成です!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】【環境構築】Docker + Ruby on Rails + ReactでSPA化

RailsとReactを使ってSPA化したWebアプリをDocker環境で作ろうとして、結構苦労したため、自分用のメモとして残します。 各種ファイルの用意 プロジェクト用のフォルダを用意して、Rails側のapiフォルダとReact側のfrontフォルダに分ける。 プロジェクト用フォルダの直下に、docker-compose.ymlを置く。 $ mkdir -p ~/project/rails-react-app $ cd ~/project/rails-react-app $ mkdir ~/project/rails-react-app/api $ touch ~/project/rails-react-app/api/Dockerfile $ touch ~/project/rails-react-app/api/entrypoint.sh $ touch ~/project/rails-react-app/api/Gemfile $ touch ~/project/rails-react-app/api/Gemfile.lock $ mkdir ~/project/rails-react-app/front $ touch ~/project/rails-react-app/front/Dockerfile docker-composeファイル作成 $ cd ~/project/rails-react-app $ vim docker-compose.yml docker-compose.yml version: '3' services: db: image: postgres volumes: - postgres-data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: 'postgres' api: build: context: ./api/ dockerfile: Dockerfile command: /bin/sh -c "rm -f /rails-react-app/tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" image: rails:dev volumes: - ./api:/rails-react-app - ./api/vendor/bundle:/rails-react-app/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development ports: - 3001:3000 depends_on: - db front: build: context: ./front/ dockerfile: Dockerfile volumes: - ./front:/usr/src/app command: sh -c "cd rails-react-app && yarn start" ports: - "8000:3000" volumes: postgres-data: driver: local RailsとReactでDockerfileを別々に用意したので、buildの部分でDockerfileの場所を指定したのがポイント。 docker-compose.yml api: build: context: ./api/ dockerfile: Dockerfile front: build: context: ./front/ dockerfile: Dockerfile Rails用のファイルの用意 ~/project/rails-react-app/api/Dockerfile FROM ruby:2.5 RUN apt-get update -qq && apt-get install -y nodejs postgresql-client RUN mkdir /rails-react-app WORKDIR /rails-react-app COPY Gemfile /rails-react-app/Gemfile COPY Gemfile.lock /rails-react-app/Gemfile.lock RUN bundle install COPY . /rails-react-app # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 # Start the main process. CMD ["rails", "server", "-b", "0.0.0.0"] Gemfile source 'https://rubygems.org' gem 'rails', '~>5' Gemfile.lock これは空ファイルを用意。 entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /rails-react-app/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" React用のファイルの用意 ~/project/rails-react-app/front/Dockerfile FROM node:12.22-alpine3.11 WORKDIR /usr/src/app コマンドの実行 React: create-react-appを用いて環境構築(参考記事: https://blog.web.nifty.com/engineer/2714 ) $ docker-compose run api rails new . --force --no-deps --database=postgresql --api $ docker-compose build $ docker-compose run --rm front sh -c "npm install -g create-react-app && create-react-app rails-react-app" api/config/database.ymlの書き換え api/config/database.yml default: &default adapter: postgresql encoding: unicode host: db username: postgres password: postgres pool: 5 development: <<: *default database: rails-react-app_development test: <<: *default database: rails-react-app_test $ docker-compose up $ docker-compose run api rake db:create $ docker-compose run api rake db:migrate これで環境構築完了! Rails: localhost:3001 React: localhost:8000 ▼参考にさせていただいた記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails部分テンプレート(パーシャル)まとめ

使い方1 before xxx.html.erb <%= @user.name %> <%= @user.email %> <%= @user.age %> after xxx.html.erb <%= render partial: 'hoge', locals: {user: @user} %> _hoge.html.erb <%= user.name %> <%= user.email %> <%= user.age %> ※ 書き方:<%= render partial: 'ファイル名', locals: { '部分テンプレート内で使う変数': '変数に入れる値' } %> ※ ファイル名は、render記載のファイルと同じディレクトリにある_hogeならhoge。 ※ localsオプションを使った場合はpartialは省略できない。 ※ partialを省略したいならlocalsオプションも省略しないといけない (例:<%= render 'hoge', user: @user %>) ※ メリット:コードがスッキリする。部分テンプレートを使いまわせるから便利&コードの保守性が上がる。 使い方2 before xxx.html.erb <% @users.each do |user| %> <%= user.name %> <%= user.email %> <%= user.age %> <% end %> after xxx.html.erb <% @users.each do |user| %> <%= render partial: 'user', user: user %> #使い方1と同じ <% end %> _user.html.erb <%= user.name %> <%= user.email %> <%= user.age %> ※ @usersの要素の個数分、部分テンプレートが呼び出されて表示される。 collectionオプションを使うと、下記のようにeach文を省略できる。 xxx.html.erb <%= render partial: 'user', collection: @users %> _user.html.erb <%= user.name %> <%= user.email %> <%= user.age %> ※ 書き方:<%= render partial: 'hoge', collection: 繰り返し表示する要素が入っているインスタンス %> ※ collectionオプションの部分にeachで回したかった変数を入れる ※ collectionに指定した変数の要素の分だけ部分テンプレートが繰り返し表示される ※ collectionオプションを使用した場合、partialで指定したファイルの名前がそのまま部分テンプレート内で使用する変数名になる。 もし別の名前として変数を使いたい場合はasに変数名を指定する <%= render partial: 'user', collection: @users, as: "hoge" %> _user.html.erb <%= hoge.name %> <%= hoge.email %> <%= hoge.age %> ※ @usersに入っている要素が一つずつ取り出され、部分テンプレート_user.html.erb内の変数hogeに代入される。 ※ 上のコード(asは除く)は以下の条件を全て満たしている時、省略できる。 ・ viewsフォルダ内にあるusersフォルダに部分テンプレート_user.html.erbが存在する ・部分テンプレート内で使う変数がuserである まとめると、views/hoges/_hoge.html.erb内で変数hogeを使っていることが省略の条件 なので、 <%= render @users %> と書けば、上述したeach文とcollection記法と意味は同じ #3つとも意味は同じ xxx.html.erb <%= render @users %> <% @users.each do |user| %> <%= render partial: 'user', user: user %> <% end %> <%= render partial: 'user', collection: @users %> users/_user.html.erb <%= user.name %> <%= user.email %> <%= user.age %> ※ 3つとも意味は同じだがパフォーマンス(実行速度)が変わってくるので注意 each文の中にrender入れる VS collectionオプションを使う eachで表示すると@usersなどに入っている要素の個数回分部分テンプレートが呼び出されるが、collectionオプションを使用して記述すると部分テンプレートが呼び出されるのは1回のみ。 なので、eachで表示するよりもcollectionオプションを使う方がパフォーマンスが良くなる。 部分テンプレートを繰り返し呼び出す時はなるべくeach文ではなくcollectionオプションを使うようにする。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む