20201208のRailsに関する記事は28件です。

GCPの非同期プロダクトからのリクエストを認証する

こちらはVISITS advent calendar 14日目の記事です。

GCPのいくつかのプロダクトでは、処理の後に予め登録しておいたエンドポイントをHTTPで呼び出すことができます。

時限的に処理を開始したい場合や何かのイベントの後に特定の処理を行いたい場合などに、プロダクトと独立させて処理を実行できるため色々と融通が効きます。

受ける側もHTTPさえ受けられれば通常のWebアプリケーションでも問題ないので便利なのですが、リクエストを送ってきた相手が本当にGCPのプロダクトなのか確認する必要があります。

これについては、2019年4月頃よりサービスアカウントを用いたトークン認証(OAuth, OIDC)ができるようになったようです。

いくつか上記の認証を紹介する記事はあったのですが、具体的な認証の実装をしているものがあまり見当たらなかったので今回rubyで書いてみることにしました。

執筆にあたっては以下の記事を参考にさせていただきました。
この記事だけ読んで一通り設定できるようにしたいため、いくつか内容が重複するところあるかと思いますがご容赦ください。

GCPのHTTP認証

GCPでは以下のプロダクトについては、エンドポイントを登録しておくことで後処理をHTTPで投げられるようになっています。

  • Cloud Scheduler
  • Cloud Tasks
  • Cloud Pub/Sub

上記のプロダクトからは以下のようなプロダクトに対して処理を投げることができます。

  • Cloud Run
  • Cloud Functions
  • Cloud Endpoints
  • その他任意のサーバー/サービス

GCPのプロダクトを組み合わせた場合、基本的に認証はGCP側でよしなにやってくれるため便利です。

またGCE/GAE/GKEといったHTTPを受けられるようなプロダクトや、GCP外の自前のサーバー等でも可能です。
ただし、この場合はエンドポイントを公開しているサーバー側で認証を対応する必要があります。

認証の流れ

実際に認証する際は以下のような流れになります。

  1. 認証用のサービスアカウントを作成する
  2. 受信側で認証する
    1. 認証機構を持つプロダクトの場合:サービスアカウントにIAMで関連のロールを付与
    2. 自前の場合:送られてくるidトークンを認証する
  3. 送信側のプロダクトにサービスアカウントを紐付ける

1. 認証用のサービスアカウントを作成する

まずはサービスアカウントを作成します。
予めロールを設定するプロダクトが分かっていれば、それにあったプロダクトのロールをここで設定しますが、後ほど設定も可能なので後回しでも大丈夫です。

今回は gcp-oidc-auth@{project-id}.iam.gserviceaccount.com のような名前にしました。

2. 受信側で認証する

続いて受信側を設定します。
送信側の設定をする際に受信側のendpointを指定するので、先に受信側を用意しておく必要があります。

2-1. 認証機構を持つプロダクトの場合

GCPプロダクトで認証できる場合は、さきほど作成したサービスアカウントに受信側プロダクトのロールを付与します。
今回は例としてCloud Runを取り上げます。

なおCloud Endpointsに関しては、違った手順で認証を構成することになります。

Cloud Runサービスの作成

まずはCloud Runに飛んでサービスを作成します。

スクリーンショット 2020-12-07 23.39.35.png

リージョンやサービス名は適当に決めて次へ。

cloud_run_detail.png

コンテナを指定するところは、適当なイメージを選択します。

詳細設定内にあるサービスアカウントは、あくまでCloud Runが何かGCPのAPIを叩くときに使うサービスアカウントになります。
1で作成したものは送信側に設定するものなので、ここではCloud Run用(もっと言うとCloud Runのサービスごと)のサービスアカウントを割り当てた方が良いと思われます。

cloud_run_auth.png

3つ目にHTTPのトリガーを指定しますが、ここで「認証を必要とする」を選択して、Cloud Runサービスを作成します。

IAMの設定

最後の「認証を必要とする」では、IAMにて送信側に設定するサービスアカウントに適切なロールを付与する必要があります。
ロールは受信側プロダクトに依存したものを付与する必要があります。

  • Cloud Run: Cloud Run 起動元
  • Cloud Functions: Cloud Functions 起動元

cloud_iam_with_cloud_run.png

2-2. 自前でIDトークンを認証する

自前でIDトークンを検証する場合は、トークンの中身について把握する必要があります。

サービスアカウントを紐付けると、そのGCPプロダクトからのリクエストのAuthorizationヘッダーにBearer Tokenがjwt形式で渡ってきます。

IDトークンをdecodeするとこのような形になります。
この場合1つ目のjsonがペイロード、2つ目がヘッダーになっています。

[
  {
    "aud": "https://hogehoge.com/path/to/endpoint", 
    "azp": "11.................52",
    "email": "hoge-service-account@{project-id}.iam.gserviceaccount.com",
    "email_verified": true,
    "exp": 1606186661,
    "iat": 1606183061,
    "iss": "https://accounts.google.com",
    "sub": "11.................52"
  },
  {
    "alg"=>"RS256",
    "kid"=>"dedc012d07f52aedfd5f97784e1bcbe23c19724d",
    "typ"=>"JWT"
  }
]

ペイロードについては公式の説明がありますので、より詳しくはそちらを参照ください。

キー 内容
aud jwtのaudクレーム。Cloud Schedulerの場合デフォルトで受信側endpointのURLが入る。
azp 独自のクレーム。認証された送信者のクライアントIDを指すらしいが、OAuthにおいてwebアプリとAndroidアプリなどで同じ人なのに違うIDで管理される場合などに使うらしい。今回は対象外か。
email 独自のクレーム。サービスアカウントが入ってくる。
email_verified ユーザー認証が済んでいればtrue。おそらくOAuthで一般ユーザーが送信する場合は認証済みでないケースは想定されるが、今回のサービスアカウントの場合は基本的にtrueのはず。
exp jwtのexpクレーム。ライブラリを使えば基本期限切れのチェックはやってくれる。
iat jwtのiatクレーム。
iss jwtのissクレーム。ID tokenの場合 https://accounts.google.comaccounts.google.com のどちらかになる。
sub jwtのsubクレーム。Googleのアカウント全体でアカウントを特定できる、ユニークなasciiコード列が入るとのこと。

ヘッダーについては、IDトークンでは現在のところRS256が使われているようです。

kidは署名に用いられた鍵を表しており、Googleが公開しているDiscoveryのjwks_uriから取得できる鍵リストの中から、一致するものをdecodeに用います。
この鍵リストは定期的に変わるようなので、cacheするとしても一定期間で取り直した方が良さそうです。

IDトークンの検証手順

IDトークンの検証手順についても公式で以下の5stepで説明されています。

  1. Google発行の証明書が用いられているか検証する
  2. issクレームがgoogleのもの (https://accounts.google.com または accounts.google.com) か検証する
  3. audクレームが送信側のプロダクトごとに設定される項目と一致するか検証する
  4. expクレームが有効期限内か検証する
  5. hdパラメータを設定している場合、hdクレームが正しいか検証する

この他、サービスアカウントの場合はemailクレームも想定したものか検証した方が良さそうです。

実装

rubyでやる場合は googleauth gem(v0.13.0以降)を利用すると便利です。
自前でやる場合は証明書の管理なども面倒ですが、その辺も全部やってくれます。

endpointを指定するということでサーバーが必要になるので、今回はRailsで書きました。

Railsで認証を行う場合はControllerにおいて、
ActionController::HttpAuthentication::Token::ControllerMethods をincludeすると

  • authenticate_with_http_token(自前で例外など処理する)
  • authenticate_or_request_with_http_token(失敗時の処理はお任せ)

などで簡単にtokenが取得できるようになります。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate!

  private

  attr_reader :oidc_token_hash

  def authenticate!
    authenticate_or_request_with_http_token do |token, _options|
      @oidc_token_hash = Google::Auth::IDTokens.verify_oidc(token, aud: request.url)

      @oidc_token_hash['email'] == ENV.fetch('GCP_SERVICE_ACCOUNT_EMAIL') # 設定値の管理はENV以外でもOK
    rescue Google::Auth::IDTokens::VerificationError => _e
      false
    end
  end

aud/iss/(azp)のクレームはgem側でやってくれるため、emailを独自にチェックするだけで済みました。

例外処理ですが、verify_oidcはトークンがおかしい場合などにVerficationError、公開鍵周りの問題でKeySourceErrorを発生させます。

前者は入力側の問題なので400系(ここではfalseを返すので401になる)として返しておき、後者は公開鍵取得に失敗した等クライアント側はどうしようもケースということで500系として検知できるようにしておきました。
この辺りの例外の取り扱いは提供するサービスのポリシーに合わせてください。

今回は例だったのでhtmlを返す形を取っていますが、通常GCPからのリクエストを処理したい場合はapi的な処理が多いと思いますので、ActionController::APIを継承しつつauthenticate_with_http_tokenで自前で処理するのも良いと思います。

3. 送信側のプロダクトにサービスアカウントを紐付ける

続いて作成したサービスアカウントを送信側プロダクトに紐付けます。
今回は例としてCloud Schedulerを取り上げます。

Cloud Schedulerの場合はAuthヘッダーでOIDCトークンを選択するところが重要です。(公式ドキュメントはこちら)

gcp_cloud_scheduler.png

ターゲットはHTTPを選択肢、URLには受信側のendpointを指定します。
またAuthヘッダーではOIDCトークンを選択し、サービスアカウントには最初に作ったアカウントを指定します。

なお、ターゲットのURLが *.googleapis.comなGoogle APIのときは、AuthヘッダーにOAuthトークンを使用するようです。

一番下にある対象の項目は後のaudクレームの値になります。
空欄の場合はターゲットのURLが入ります。

後はcronでも手動でもいいので実行し、認証が成功するかを確認します。

cloud_scheduler_job.png

Cloud Pub/SubやCloud TasksなどもHTTPターゲットの設定とサービスアカウントが設定できるので、同様の設定で大丈夫です。

おわりに

ということでGCPの非同期系プロダクトからのリクエストを認証する設定の流れについてでした。

受信側にもし認証機構をもつプロダクトを割り当てられる場合はそちらを選択した方が楽ではありますが、idトークンの認証自体もそれほど複雑ではないので、ちゃんと導入してセキュアな状態を保ちたいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ymlファイルの中でerbを使い、動的に値を取得してrakeタスクの引数にする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] モデルの作成方法

役割

モデルの役割はデータベース・テーブルを使って「データを管理する」ことです。

作成方法

rails g model モデル名

モデルや、それに付随するファイルを一度に作成できるコマンドです。

モデルの命名規則は単数形を使用するです。
なので、商品の情報を管理する場合は「product」のように命名しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseを利用した新規登録時のユーザー情報の保存とフラッシュメッセージについて

はじめに

deviseを利用したuser周りの設定の中で、私が困ったことについて解決方法を記述しようと思います。

今回なかなか思った通りにできずに困ったことはユーザーの新規登録です。新規登録ページの作成はできましたが、そこから先で躓いてしまいました。

  • 登録したい新規ユーザーの情報が登録されない
  • 新規登録ページで情報入力後、ボタンを押してもページが変わらない
  • 新規登録ができた場合とできなかった場合の動作を変えたい
  • 新規登録時のフラッシュメッセージの表示がされない

以上を解決した方法について備忘録として記事を書きました。

Rails 5.2.4.4
Ruby 2.5.1
を使用しています。

ルーティングの設定

最初に躓いたのはユーザー情報が保存されないことです。sendボタンを押すとそのまま動かなくなり、Sequel Proを確認しても情報の保存がされていませんでした。

ルーティングとコントローラーに問題があると考えたので、まずはroutes.rbを確認します。

現状ではおそらく既にご自身で設定したトップページなどへのルーティングと、devise導入時に自動で記述されたコードで以下のようになっていると思います。

config/routes.rb
devise_for :users

root "トップページ"
resources :その他のページ

ここに追記をしていきます。
新規登録はdeviseで自動生成されたregistrationsにあたるので以下のように記述をします。新規登録には関係ありませんが、ついでにログインに必要なsessionsについても記述しておきましょう。また、resources :usersという記述も追加します。

config/routes.rb
devise_for :users, controllers: {
  registrations: 'users/registrations',
  sessions: 'users/sessions'
} 

root "トップページ"
resources :その他のページ
resources :users

コントローラーの設定

次にコントローラーを確認します。

registrations_controller.rbを開きます、こちらはdeviseで自動生成されたファイルです。
中を見ると class ~ end の中身が全てコメントアウトされているのが確認できます。こちらにコードを記述をしていきます。

今回やりたいことはユーザー新規登録なので、newとcreateを記述します。

app/controllers/uses/registrations_controller.rb
def new
  @user = User.new
end

def create
  @user = User.new(user_params)
  if @user.save
    redirect_to root_path
  else
    render :new
  end
end

private

def user_params
  params.require(:user).permit(:email, :password, :password_confirmation)
end

createの中では複数のことをしているので注意点の記述や解説をします。

まず

app/controllers/uses/registrations_controller.rb
@user = User.new(user_params)

この(user_params)についてははprivate以下で記述しています。

app/controllers/uses/registrations_controller.rb
def user_params
  params.require(:user).permit(:email, :password, :password_confirmation)
end

このpermitの後の部分では新規登録時に必要なカラムを記述します。例えば私の場合はニックネームも登録できるようにしたかったので、以下のように記述をしました。

app/controllers/uses/registrations_controller.rb
def user_params
  params.require(:user).permit(:nickname, :email, :password, :password_confirmation)
end

ご自身の登録したい情報によって記述を変更してください。

次の記述です。

app/controllers/uses/registrations_controller.rb
if @user.save
  redirect_to root_path
else
  render :new
end

この記述は新規登録ができた場合とできなかった場合の条件分岐をしています。
こちらでは登録ができた場合はトップページへ飛び、できなかった場合にはまたユーザー新規登録ページへと戻ってくるように記述しています。

redirect_to と render は似たような動きをしますが使い分けが必要です。こちらの記事がとてもわかりやすく解説されているので、気になる方は目を通してみると勉強になると思います。
https://qiita.com/morikuma709/items/e9146465df2d8a094d78

ここまでで以下の問題が解決しました。

  • 登録したい新規ユーザーの情報が登録されない
  • 新規登録ページで情報入力後、ボタンを押してもページが変わらない
  • 新規登録ができた場合とできなかった場合の動作を変えたい

新規登録ページから情報を入力して登録ができること、登録が失敗した場合にはまた新規登録ページに戻ってくることを確認してください。問題なく行えていれば成功です。

しかし今のままでは新規登録が成功してもトップページに飛ぶだけなので、ちゃんと登録ができたのかユーザーにはとてもわかりにくい状態です。
最後にフラッシュメッセージを表示できるようにします。

フラッシュメッセージを表示する

deviseでは設定をすれば簡単にフラッシュメッセージを表示できます。
こちらの記事がフラッシュメッセージの導入方法についてわかりやすく解説してあります。
https://qiita.com/hari00702/items/4e100b9dc78d19e8e316

しかし、ユーザー新規登録時の登録完了のメッセージはその中には入っていません。
そこでregistrations_controller.rbのcreateに以下のように追記します。

app/controllers/uses/registrations_controller.rb
def create
  @user = User.new(user_params)
  if @user.save
    redirect_to root_path, notice: 'ユーザー新規登録を完了しました' #追記
  else
    render :new
  end
end

notice: の後ろの部分の記述がそのままフラッシュメッセージになるので好みのメッセージを入れてください。
新規登録ページから実際に登録を行ってみて、ページ上部にメッセージが表示されていれば成功です。

これで残りの

  • 新規登録時のフラッシュメッセージの表示がされない

についても解決できました。

参考

https://qiita.com/mmmasuke/items/7d5c47b4a40f6912adf2

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rspec問題2

事前準備

ターミナル上で下記コマンドを実行してcloneする。

git clone https://github.com/Shu-Hos/pictweet_error.git
cd pictweet_error
bundle install
yarn install
rails db:create
rails db:migrate

問題1

ターミナルで下記コマンドを実行してください。

bundle exec rspec spec/models/user_spec.rb

するとNoMethodError:
undefined method name=' for #<User:xxxxxxx>’
のエラーが発生するかと思います。
まずはこちらをヒントなしで解決しましょう。

問題2

上記のエラーが改善されて再度こちらのコマンドを実行してください

bundle exec rspec spec/models/user_spec.rb

しかしテストの結果は、Failure/Error:expect(user.errors[:nickname]).to include("is too long (maximum is 6 characters)")が残ってしまっているかと思います。
こちらを修正してuserのモデル単体テストを成功させてください。

ヒント

binding.pryでuserの中身を確認すると条件分岐の結果がtrueになっているかつエラーメッセージが空になっているかと思います。
バリデーションが怪しいかも?

問題3

問題2まで解き終えたら次はtweetのmodel単体テストです。
以下のコマンドを実行してください。

bundle exec rspec spec/models/tweet_spec.rb

すると正常系のテストコードがすべてfailuresになってしまいます。
こちらの修正をお願いします。

ヒント

binding.pryで@tweetの中身を確認してみましょう。

問題4

次はtweets_controllerの単体テストです。
以下のコマンドを実行してください。

bundle exec rspec spec/requests/tweets_spec.rb

すると、ActionController::UrlGenerationErrorが発生するかと思います。
エラー文をよく読んでこちらの問題を解決しましょう。

ヒント

エラー文はPNo route matches {:action=>"show", :controller=>"tweets"}, missing required keys: [:id]と出ているはずです。必要に応じてrails routesコマンドでshowアクションのパスを確認してみましょう。

ここからはtweet機能の結合テスト問題です。

問題5

以下のコマンドを実行してください。

bundle exec rspec spec/system/tweets_spec.rb

ログインページでログインができずにCapybara::ElementNotFound:Unable to find field "email" that is not disabledというエラーが発生するかと思います。
まずこちらを解決しましょう。

ヒント

ElementNotFoundと出ているので検証ツールで要素の中身を確認してみましょう。

問題6

こちらで最後の問題になります!
再度以下のコマンドを実行してください。

bundle exec rspec spec/system/tweets_spec.rb

以下のエラーが起きたかと思います。
Failure/Error: expect(page).to_not have_no_selector 'form'expected to find css "form" but there were no matches

ヒント

テストの実行文とexpectedの実行結果の英文の意味を調べてみると良いかも知れないです。


お疲れさまです!!
何かご不明点とかあれば、今後ぜひ教えてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

formオブジェクトで複数テーブルへ値の保存

formオブジェクトを使った複数テーブルへの値の保存を1週間位試行錯誤してやっと実装できたので、記事に残しておこうと思います。
色々な方の実装方法を真似て作ったので解釈が間違っているところや、理解が不十分なところもありますが、自分はこう解釈してこの記述をしているということで書いていきます。

前提
プログラミング初学者の備忘録的な感じで書いており、間違いがある場合がありますので、ご注意ください。
解釈やそもそもの定義など間違っている部分等ありましたらご指摘していただけると幸いです。
ターミナルでのファイル生成の記述については全て省略しています。

動作環境

macOS Catalina 10.15.7
Rails 6.0.3.4
Ruby 2.6.5p114

formオブジェクトって何?

デザインパターンの1つで、1つの投稿フォームから複数のモデルに関連するデータを更新できるものです。
簡単に言うと、1つの投稿フォームから複数のテーブルへの保存の処理をするまとめ役みたいな感じです。
そのため、formオブジェクトにはform_withメソッドに対応する機能と、バリデーションを行う機能をもたせることが必要になります。
form_withで複数のテーブルに保存するデータをformオブジェクトに送り、
届いたデータに対して、各モデルのバリデーションをformオブジェクトで行った後に複数のテーブルへデータを保存すると言った流れです。

formオブジェクトを使ったフォームの実装

今回は以下2つをポイントに実装を行いました。
formオブジェクトで記事の新規投稿、更新処理ができる。
1つの入力フォームから複数のタグの新規登録、更新処理ができる。
記事の新規投稿のみに比べ、更新処理も実装するとかなり手間がかかりました。

ER図と実際の投稿フォーム

スクリーンショット_2020-12-07_19_52_31(2).png

ER図はこのような感じで、赤枠の部分をformオブジェクトで実装しました。
formオブジェクトを使い、1つの投稿フォームでarticleとtagを保存するような設計です。
具体的な動きとしては、下図のようなフォームで記事にタグを付けて投稿し、記事とタグをそれぞれのテーブルに保存します。
HabitApp.png
ArticleとTagを紐付けるために中間テーブルとして、article_tag_relationsテーブルを作っています。

モデルについて

各モデルは以下のように記述しました。
マイグレーションファイルもカラムの部分のみをモデルの下に書いています。

Articleモデル(app/models/article.rb)

class Article < ApplicationRecord
  has_many :article_tag_relations, dependent: :destroy
  has_many :tags, through: :article_tag_relations
end

マイグレーションファイル
 t.string :title,  null: false
 t.text :output,   null: false
 t.string :action
 t.integer :user_id

Tagモデル(app/models/tag.rb)

class Tag < ApplicationRecord
  has_many :article_tag_relations, dependent: :destroy
  has_many :articles, through: :article_tag_relations

  validates :tag_name, uniqueness: true
end

マイグレーションファイル
t.string :tag_name, uniquness: true

ArticleTagRelationモデル(app/models/article_tag_relation.rb)

class ArticleTagRelation < ApplicationRecord
  belongs_to :article
  belongs_to :tag
end

マイグレーションファイル
t.references :article, foreign_key: true
t.references :tag, foreign_key: true

モデルについては2つのポイントがあり

dependent: :destroy

1つは、articleが削除されたとき、tagが削除されたときには中間テーブルのarticle_tag_relationから削除された値が含まれるレコードも削除されるようにしました。

validates :tag_name, uniquness: true

もう1つは、同じ名前のtagが複数保存されないようにしています。

コントローラーについて

コントローラーは以下のように記述しました。

class ArticlesController < ApplicationController
  before_action :authenticate_user!, only: [:new, :edit, :update, :destroy]
  before_action :set_article, only: [:show, :edit]

  def index
    @articles = Article.all.order('created_at DESC')
  end

  def new
    @article_tag = ArticleTag.new
  end

  def create
    @article_tag = ArticleTag.new(article_params)
    tag_list = params[:article][:tag_name].split(',')
    if @article_tag.valid?
      @article_tag.save(tag_list)
      redirect_to articles_path
    else
      render :new
    end
  end

  def show
  end

  def edit
    @article = Article.find(params[:id])
    @article_tag = ArticleTag.new(article: @article)
  end

  def update
    @article = Article.find(params[:id])
    @article_tag = ArticleTag.new(article_params, article: @article)
    tag_list = params[:article][:tag_name].split(',')
    if @article_tag.valid?
      @article_tag.save(tag_list)
      redirect_to article_path(@article)
    else
      render :edit
    end
  end

  def destroy
    @article = Article.find(params[:id])
    redirect_to root_path if @article.destroy
  end

  private

  def article_params
    params.require(:article).permit(:title, :output, :action, :user_id, :article_id, :tag_name, :tag_id).merge(user_id: current_user.id)
  end

  def set_article
    @article = Article.find(params[:id])
  end

end

コントローラーについては特に変わったところはなく、
tag_listがformオブジェクトで値を保存するために定義したsaveメソッドに使われるくらいです。

formオブジェクトの作成

formオブジェクトはapp/formsディレクトリを作成し、その直下にarticle_tag.rbというファイル名で作成しました。

class ArticleTag
  include ActiveModel::Model
  attr_accessor :title, :output, :action, :tag_name, :user_id, :tag_id, :article_id

  with_options presence: true do
    validates :title, length: { maximum: 40 }
    validates :output, length: { maximum: 400 }
  end

  # レコードに値があるかないかでcreateかupdateかに分岐させる
  delegate :persisted?, to: :article

  def initialize(attributes = nil, article: Article.new)
    @article = article
    attributes ||= default_attributes
    super(attributes)
  end

  def save(tag_list)

    ActiveRecord::Base.transaction do
      @article.update(title: title, output: output, action: action, user_id: user_id)

      current_tags = @article.tags.pluck(:tag_name) unless @article.tags.nil?
      old_tags = current_tags - tag_list
      new_tags = tag_list - current_tags

      old_tags.each do |old_name|
        @article.tags.delete Tag.find_by(tag_name: old_name)
      end

      new_tags.each do |new_name|
        article_tag = Tag.find_or_create_by(tag_name: new_name)
        @article.tags << article_tag
        article_tag_relation = ArticleTagRelation.where(article_id: @article.id, tag_id: article_tag.id).first_or_initialize
        article_tag_relation.update(article_id: @article.id, tag_id: article_tag.id)
      end

    end
  end

  def to_model
    article
  end

  private

  attr_reader :article, :tag

  def default_attributes
    {
      title: article.title,
      output: article.output,
      action: article.action,
      tag_name: article.tags.pluck(:tag_name).join(',')
    }
  end
end

include ActiveModel::Model

ActiveModel::Modelというモジュールをincludeメソッドで与えます。
この記述によって ArticleTagクラスがモデルとしての機能を行えるようになります。
form_withへの対応やバリデーションを行うために必要な記述です。

delegateメソッド

指定したオブジェクトにメソッドの実行を委譲させるものです。
委譲:あるオブジェクトの操作を一部他のオブジェクトに代替させる手法
言葉が難しいです。。。

delegate :メソッド名, to: :委譲先のオブジェクト

ここではpersisted?メソッドをarticleというオブジェクトに委譲しています。
to_modelメソッドと合わせて、
レコードに値が存在しないときにcreateアクション
レコードに値が存在するときはupdateアクションを動かすために必要な記述になります。

to_model

モデルであるためには、to_modelを定義する必要があります。
コントローラーやview helperにモデルが渡ったときにto_modelを呼んでモデルを操作するためです。

理解が浅いためうまく説明できないのですが、delegateメソッドで値があるときと無いときに応じてPOSTやPATCHの処理を切り替え、
to_modelメソッドはアクションのURLを適切な場所に切り替えているということらしいです。

initializeメソッド

initializeメソッドはnewメソッドでインスタンスを生成する時に初期値で設定する値などを定義するメソッドです。
attributesは属性値の意味で、

attributes ||= default_attributes
super(attributes)

arrtibutesが存在すればその値を、nilであれば、default_attributesをattributesに代入するといった記述です。
superは、スーパークラスを呼び出す記述です。
initializeをここでは再定義(オーバーライド)していますが、オーバーライドする前のinitializeメソッドを引数をattributesとして呼び出しています。
単にnewメソッドでインスタンスを生成するわけではなく、
attributesが存在すればその値を使ってインスタンスを生成
存在しなければ、default_attributesを使ってインスタンスを生成するといった記述です。
createアクションとupdateアクションを値のあるなしで使い分けるための記述を1つにまとめるために、initializeの再定義を行っていると思います。
投稿のみであれば、ここの記述は必要ありません。
更新にも対応するためにレコードに保存された値を取得するために定義しています。

default_attributes

privateメソッド以下にあるdefault_attributesは投稿フォームに入力する値のdefault値を定義しました。
articleとtagを使ってdefalut値を設定するためにattr_readerでarticleとtagを読み込んでいます。
ここでは書き込む必要はなく、値として読み込むだけの処理のためattr_readerで十分になります。

saveメソッド

saveメソッドは新規投稿と更新の両方をこのメソッド1つで行うことができます。

initializeを再定義したので、newアクションでdefault_attributesの値が入ったレコードが生成されます。
フォームの入力値をarticle_paramsとして取り出して、以下のコードで入力した値に更新するという処理にして新規登録します。

@article.update(title: title, output: output, action: action, user_id: user_id)

新規登録の場合は、defalut_attributesとして全ての値がnilのレコードが生成され、フォームの入力値(article_params)で更新するといった流れです。
更新処理の場合は、保存されたレコードの値がdefault_attributesとしてあり、フォームの入力値(article_params)で更新すると言った流れです。
タグの部分の記述については後述します。

ActiveRecord::Base.transaction

トランザクションの処理を記述する時に使うものです。
トランザクションとは、分割できないワンセットの処理単位のことです。
この中に書かれた処理で途中で例外処理(エラー)があったときには途中までの処理や結果はやらなかったことにするというものです。
ここではActiveRecord::Base.transaction doからendまでの処理
つまり、articleとtagの新規登録、更新処理がトランザクションになっています。
articleとtagの新規登録や更新する時にどこかで処理が失敗した場合、途中までやっていた処理はすべてなかったことにするということです。

タグの扱いについて

タグについては複数のタグを登録、編集できる機能にしました。
tag_list(フォームに入力したタグ)
current_tags(現在保存されているタグ、更新の場合のみ使われる変数)
old_tags(現在保存されていて、そのまま残すタグ)
new_tags(新しく保存されるタグ)
の4つを配列で定義し、配列内の各要素を保存するという流れです。

tag_list = params[:article][:tag_name].split(',')

上記の記述では、フォームで送信されたparamsからタグの値を取り出します。
このときsplit(',')では入力した値を,で区切って要素に分解して配列にするといった処理が行われます。
例えばタグを入力するフォームに

朝,昼,夜

と入力した場合、

tag_list = ["朝","昼","夜"]

と入ることになります。

新規登録の場合

current_tags = @article.tags.pluck(:tag_name) unless @article.tags.nil?

unless @article.tag.nil?
となっており、タグが空でない場合にcurrent_tagsが定義されるため新規登録の場合は定義されません。
new_tags = tag_listとなり以下の処理に移ります。

new_tags.each do |new_name|
   article_tag = Tag.find_or_create_by(tag_name: new_name)
   @article.tags << article_tag
   article_tag_relation = ArticleTagRelation.where(article_id: @article.id, tag_id: article_tag.id).first_or_initialize
   article_tag_relation.update(article_id: @article.id, tag_id: article_tag.id)
end

2行目:find_or_create_byメソッドではTagモデルを通じてTagsテーブルから、tag_nameがnew_nameのものを探し、なければその値を保存します。
3行目:次の行で保存されたarticle_tagを@article.tags、つまり投稿した記事のタグの配列に格納します。
4行目:中間テーブルに値を保存する処理です
first_or_initializeメソッドは新規登録の場合はinitializeつまり新しくレコードが生成され、更新の場合はレコードは生成されません。
5行目:生成したレコード、元々あったレコードをupdateメソッドで更新する
と言った流れで新規登録されます。

更新の場合

更新の場合はtag_listとcurrent_tagsを使って、
編集された時に削除されたタグを中間テーブルから削除し、新しく追加されたタグをTagsテーブルと中間テーブルに保存する必要があります。

current_tags = @article.tags.pluck(:tag_name) unless @article.tags.nil?
old_tags = current_tags - tag_list
new_tags = tag_list - current_tags

例として、元々登録していたタグをtag1,tag2とします。
1行目:投稿した@articleに紐づくtag達を配列形式で取得します。
tagsテーブルからpluckメソッドでtag_nameというカラムを指定し、投稿した記事につけたタグのtag_nameの値を配列に格納します。
2行目:元々登録していたけれど、編集によって削除されたタグをold_tagと定義しています。
例えば、元々tag1,tag2があって編集画面でタグの欄をtag1だけにした場合はtag_listはタグのフォームに入力された値であるため、
old_tags = ["tag1", "tag2"] - ["tag1"] =["tag2"]となります。
3行目:元々登録されていなかった新規のタグをnew_tagsと定義しています。
例えば、元々tag1,tag2があって編集画面でtag1,tag2,tag3とした場合、
new_tags = ["tag1", "tag2", "tag3"] - ["tag1", "tag2"] = ["tag3"]となります。

old_tagsについての処理

old_tagsはフォームから削除され、投稿につけなくなったタグです。
投稿に紐づくタグとして以下の記述で削除する必要があります。

old_tags.each do |old_name|
  @article.tags.delete Tag.find_by(tag_name: old_name)
end
new_tagsについての処理

new_tagsは新たに追加したタグなので、新規のタグの場合はTagsテーブルに保存する必要があります。
また、投稿に紐づくタグとして新たに中間テーブルに保存する必要があります。
処理の内容については新規登録の場合の説明と全く一緒です。

まとめと感想

簡単なまとめ

formオブジェクトは1つのフォームから複数のテーブルに値を保存するために使われるデザインパターンの1つ
複数のタグを保存するにはpluckメソッドやsplitメソッドをうまく使って配列に格納し、eachメソッドを使ってそれぞれのタグに保存処理を行う

感想

delegateとto_modelメソッドについてなんとなく意味は分かった気がするが、formオブジェクトに記述して細かい部分でどう動いているのか完全には理解できていないので、もう少し理解を深める。
to_modelメソッドはActiveModel::Conversationに含まれるメソッドということで、他にもよく使っているメソッドがあるため今後勉強していく。

実装内容をすべて書いたのでものすごく長くなりました。
解釈間違い等ありましたらコメントしていただけると幸いです。
記事にわかりやすくまとめる技術も学んでいかなければ。。。

参考記事

formオブジェクトについて
https://product-development.io/posts/rails-design-pattern-form-objects
https://tomo-bb-aki0117115.hatenablog.com/entry/2020/10/29/232822

タグ付け機能
https://qiita.com/E6YOteYPzmFGfOD/items/bfffe8c3b31555acd51d

トランザクションについて
https://wa3.i-3-i.info/word142.html

to_modelについて
https://www.bokukoko.info/entry/2015/12/20/Rails_%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AB%E9%96%A2%E3%81%97%E3%81%A6

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 6: ActionCableとVue.jsで非同期処理を行うサンプル

環境: Rails 6.0、Vue.js 2.6、ソース: https://github.com/kazubon/cable60

ActionCable、ActiveJob、およびVue.jsを使って、次のような非同期処理を行う画面を作ります。
image.png

必要なライブラリ

GemfileにRedisとSidekiqを追加して、bundle install してください。

gem 'redis', '~> 4.0'
gem 'sidekiq'

Redisをインストールしていない場合は、インストールして起動しておきます。

% brew install redis
% brew services start redis

ActionCableの準備

cable.ymlで、development環境の設定を async から redis に変えます。サンプルでは、ActiveJobのジョブの中でActionCableのブロードキャストを行いますが、async だと効かないからです。

config/cable.yml
development:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: cable60_development

コントローラでは、ActionCable用の接続の識別子となるランダム文字列をクッキーに入れます。このサンプルでは、「1ユーザー - 1識別子 - 1ストリーム」とします。1対多の送信は行いません。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_cable_code

  private
  # Action Cable用ユーザー識別
  def set_cable_code
    cookies.signed[:cable_code] ||= SecureRandom.hex
  end
end

connection.rbではクッキーから識別子 cable_code を取り出します。

app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :cable_code

    def connect
      if cookies.signed[:cable_code]
        self.cable_code = cookies.signed[:cable_code]
      end
    end
  end
end

bin/rails g channel user で user_channel.rb を作っておき、識別子 cable_code を stream_from にそのまま渡します。

app/channels/user_channel.rb
class UserChannel < ApplicationCable::Channel
  def subscribed
    stream_from cable_code if cable_code
  end

  def unsubscribed
  end
end

ActiveJobの準備

config/environments下のdevelopment.rbとproduction.rbを修正し、ActiveJobではSidekiqを使うことを指定します。

config/environments/development.rb
  config.active_job.queue_adapter     = :sidekiq

コントローラでは、「開始」ボタンで呼び出す update アクションを書いておきます。SampleJobというジョブにクッキーの識別子を渡して非同期処理をさせます。

app/controllers/samples_controller.rb
class SamplesController < ApplicationController
  def show
  end

  def update
    SampleJob.perform_later(cookies.signed[:cable_code])
    render json: {}
  end
end

bin/rails g job sample でSampleJobを作っておいて、performメソッドにサンプル用の処理を書きます。20回スリープしながら現在のパーセンテージを進めるだけのものです。

ActionCableのブロードキャストを使い、識別子 cable_code に対してハッシュ(JavaScriptのオブジェクト)を送信します: { type: 処理の種類に付けた名前, progress: パーセンテージ, processing: 処理中かどうか }

app/jobs/sample_job.rb
class SampleJob < ApplicationJob
  queue_as :default

  def perform(cable_code)
    20.times do |idx|
      sleep 0.2
      ActionCable.server.broadcast(cable_code,
        type: 'sample', progress: (idx + 1) * (100 / 20), processing: true)
    end
    ActionCable.server.broadcast(cable_code,
      type: 'sample', progress: 100, processing: false)
  end
end

JavaScriptでデータを受け取り、進行状況を表示する

JavaScript側では、ActionCableからデータを受け取るためのオブジェクトを作ります。Vue.observable を使うことで、sampleプロパティを変更したらVueのテンプレートに反映するようにします。

ほかに非同期処理を扱う画面が増えたら、fooとかbarとかプロパティを増やすことを想定しています。

app/javascript/channels/cable_data.js
import Vue from 'vue';

export default Vue.observable({
  sample: { },
  // foo: { },
  // bar: { },
})

UserChannelに対応するuser_channel.jsを修正します。receivedでデータを受け取ったら、ActionCable用のオブジェクトcableDataのプロパティにそのまま入れます。

オブジェクトのtypeプロパティの値がcableDataの各プロパティの名前に対応していることにします。

app/javascript/channels/user_channel.js
import consumer from "./consumer"
import cableData from "./cable_data";

consumer.subscriptions.create("UserChannel", {
  connected() {
  },

  disconnected() {
  },

  received(data) {
    switch(data.type) {
      case 'sample':
        cableData.sample = data;
        break;
      // case 'foo':
      //   cableData.foo = data;
      //   break;
      // case 'bar':
      //   cableData.bar = data;
      //   break;
    }
  }
});

非同期処理の進行状況を表示するVueコンポーネントです。ActionCable用のオブジェクト cableData.sampleのprogressとprocessingの値を画面に反映させます。

なお、ここではBootstrapのProgressを使っています。

app/javascript/sample.vue
<template>
  <div>
    <div class="form-group row">
      <div class="progress">
        <div class="progress-bar" role="progressbar" :aria-valuenow="progress"
          aria-valuemin="0" aria-valuemax="100" :style="`width: ${progress}%`"></div>
     </div>
    </div>
    <div class="form-group row">
      <button type="button" class="btn btn-primary" @click="startProcess"
        :disabled="processing">開始</button>
    </div>
  </div>
</template>

<script>
import Axios from 'axios'
import cableData from "./channels/cable_data"

export default {
  data() {
    return {
    };
  },
  computed: {
    progress() {
      return cableData.sample.progress || 0;
    },
    processing() {
      return cableData.sample.processing;
    }
  },
  methods: {
    startProcess() {
      cableData.sample = { progress: 0, processing: true };
      Axios.patch('/sample');
    }
  }
}
</script>

<style scoped>
.progress {
  width: 100%;
}
</style>

サンプルのVueコンポーネントをマウントするpacks下のJavaScriptです。require("channels") ががあることを確認しましょう。

app/javascript/packs/application.js
import 'bootstrap';
import '../stylesheets/application';

require("@rails/ujs").start()
require("turbolinks").start()
// require("@rails/activestorage").start()
require("channels")

import Vue from 'vue';
import TurbolinksAdapter from 'vue-turbolinks';

import Sample from '../sample.vue';
import '../axios_config';

Vue.use(TurbolinksAdapter);

document.addEventListener('turbolinks:load', () => {
  if(document.getElementById('sample')) {
    new Vue(Sample).$mount('#sample');
  }
});

bin/rails s でサーバーを起動し、別のターミナルで bundle exec sidekiq を起動すれば、非同期処理の動作を確認できます。

Vue.observable を使わない場合

上記のcable_data.jsで、Vue.observableを使わずにJavaScriptのオブジェクトをそのままエクスポートしても、ActionCableのデータを扱えます。

app/javascript/channels/cable_data.js
export default {
  sample: { }
}

この場合は、Vueコンポーネントでdataを使ってオブジェクトを渡せば、sampleプロパティの変更が反映されます(リアクティブになります)。

app/javascript/sample.vue
<script>
import Axios from 'axios'
import cableData from "./channels/cable_data"

export default {
  data() {
    return {
      cableData: cableData
    };
  },
  computed: {
    progress() {
      return this.cableData.sample.progress || 0;
    },
    processing() {
      return this.cableData.sample.processing;
    }
  },
  methods: {
    startProcess() {
      this.cableData.sample = { progress: 0, processing: true };
      Axios.patch('/sample');
    }
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでHTMLファイルを出力する

やりたいこと

テンプレートに沿ったHTMLファイルを出力したい!

前提条件

以下のコマンドでBooksControllerを作成し、その中にHTMLファイルを出力するアクションを作成する。

  rails g controller books

テンプレートファイルを作成する

今回は以下のディレクトリを作成し、その中にテンプレートファイルを保存する。
app/views/books/template

html_template.html.erb
<!DOCTYPE html>
  <head>
    <meta charset="UTF-8">
    <title>本の詳細</title>
  </head>
  <body>
    <table>
      <tr>
        <th>タイトル</th>
        <td><%= @title %></td>
      </tr>
    </table>
  </body>
</html>

アクションを作成する

以下のようなアクションを作成する。

books_controller.rb
  def htmlfile_download
    @title = "本の題名"

    # 指定したファイルの中身を文字列で返す
    # layoutオプションの値をfalseにしておくと、レイアウトが適用されていない状態で取得できる
    template = render_to_string('books/template/html_template', layout: false)

    # HTMLファイルを生成
    send_data(template, filename: "ファイル名.html")
  end

以上です。

おまけ

render_to_stringで取ってきた値は以下のようになっています。

<!DOCTYPE html>
  <head>
    <meta charset="UTF-8">
    <title>本の詳細</title>
  </head>
  <body>
    <table>
      <tr>
        <th>タイトル</th>
        <td>本の題名</td>
      </tr>
    </table>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Active Storageで動画をアップする!!

Active Storageで動画をアップする!!

Active Storageを使用。動画をアップロードし、validatesで形式を指定する!!

お師匠方初めまして!
Active Storageはかなり便利で画像の際かなり使い勝手が良いですよね。
今回は動画にチャレンジでバリデーションまで設定しました。
他にも皆さんのオススメなどあったら聞きたいです^^

https://gyazo.com/ab51ad2729703e1b77f831205fef7550

該当するソースコード

#app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_one_attached :video
  with_options presence: true do
    validates :title
    validates :price, format: { with: /\A[-]?[0-9]+(\.[0-9]+)?\z/}
    validates_inclusion_of :price, in: 500..5000
    validates :video
  end
  validate :video_type

  private

  def video_type
    if !video.blob.content_type.in?(%('video/quicktime video/quicktime'))
        errors.add(:video, '動画は携帯で撮影したmov形式でアップロードしてください')
    end
  end
end
#app/views/posts/index.html.erb
class PostsController < ApplicationController
#省略
<video src=<%= rails_blob_path(post.video) %> type="video/mov", controls></video>

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Active Storageで動画をアップ

★Active Storageは画像、動画が投稿できる

ab51ad2729703e1b77f831205fef7550.png

❶バリデーションに形式を記述

#app/models/post.rb
class Post < ApplicationRecord
#省略
  validate :video_type

  private

  def video_type
    if !video.blob.content_type.in?(%('video/quicktime video/quicktime'))
        errors.add(:video, '動画は携帯で撮影したmov形式でアップロードしてください')
    end
  end
end

❷ビューファイルにvideoタグを記述

#app/views/posts/index.html.erb
class PostsController < ApplicationController
#省略
<video src=<%= rails_blob_path(post.video) %> type="video/mov", controls></video>

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]ransackでの月度検索の実装(Custom Predicates)

概要

ransackを使用している際、月度検索を使用したくなったので以下の通り実装した。

↓例:2020年12月を選択したら、2020/12/1-2020/12/31まで検索する
Image from Gyazo

実装

検索用の述語(Predicate)を作成する。
add_predicateで述語を追加、述語名は「during_month」で実装。

# config/initializers/ransack.rb

Ransack.configure do |config|
  config.add_predicate :during_month,
                        :arel_predicate => :between,
                        :formatter => proc { |v|
                          unless v.month == 12 # 12月度以外の処理
                            #月末日をenddayに代入 v.monthを+1して最後にdateを1引くことで月末日を取得している。
                            endday = Time.zone.parse("#{v.year}-#{v.month + 1}-1").to_date - 1
                          else # 12月度の処理
                            # 12月度の時はv.monthはインクリメントしない(エラー回避)
                            # v.yearを+1で翌年に、monthは1月固定、
                            # 最後に1日に固定したdateを1引くことで12月の末日をenddayに代入している。
                            endday = Time.zone.parse("#{v.year + 1}-1-1").to_date - 1
                          end
                          #検索月の初日から月末日まで
                          Time.zone.parse("#{v.year}-#{v.month}-1").to_date..endday
                        },
                        :type => :date
end

View側の実装

<%= search_form_for "検索オブジェクト", url: "アクション" do |f| %>
  <%= f.label :search_day, "期間"%>
  <%= raw sprintf(
      f.date_select(
        :search_day_during_month,
        discard_day: true,
        use_month_numbers: true,
        date_separator: '%s'),
      '年 ') + '月' %>
  <div class="actions ransack-submit"><%= f.submit "検索" %></div>
<% end %>

参考

ransackで年度検索を実装する
https://qiita.com/tanakaworld/items/c7a2613589dadbb2ef4d

Custom Predicates · activerecord-hackery/ransack Wiki
https://github.com/activerecord-hackery/ransack/wiki/Custom-Predicates

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails コマンドへ独自コマンドを組み込む方法

Rake タスクって何だか変ですよね。テストも書きづらいし、できることなら書きたくないですよね。

Ruby には Thor というイケてる Gem があり、これを利用するとイケてるコマンドライン・ユーティリティを書くことができます。Thor はイケてるので、サブコマンドも書くことができます。Thor を利用する場合、コマンドライン・ユーティリティは、Thor クラスを継承したクラスとして作成するので、テストも簡単で、特別な知識は不要です。

Rake タスクではなくて、Thor を利用できたら素敵ですよね。

実は rails コマンドは Thor をすでに利用しているんです。そして、Rails エンジンの場合、Thor を使って独自コマンドを提供する標準的な方法が用意されているようですが、Rails アプリケーションの場合、標準的な方法はありません。自分でなんとかするしかありません。

以降では自分でなんとかする方法を説明します。generator のアシストは受けられないので、全て手作業でファイルを修正したり、ファイルを作成したりする必要があります。

前提

本記事で作成したサンプルは https://github.com/sunny4381/rails_command_extension に置いておきます。
このサンプルは Rails チュートリアル第14章のソースコードを元にしています。Rails チュートリアルでは UserMicropost の二つのモデルが登場しますので、本記事もこの2つのモデルを操作してみたいと思います。

Rails::Command::Base

早速、独自コマンドを作成していきます。独自コマンドは、直接 Thor を継承せずに、Rails が Thor をラップしたクラス Rails::Command::Base を提供しているので、このクラスを継承するようにします。

早速、独自コマンドを実装しましょう。以下のような main_command.rb ファイルを作成し、このファイルに独自コマンドを実装していきます。

lib/commands/main/main_command.rb
require "rails/command"

module SampleApp
  module Command
    class MainCommand < Rails::Command::Base
      namespace "sample"
      @command_name = "sample"

      def hello
        say "hello"
      end
    end
  end
end

namespace@command_name を指定して、コマンド名を明示的に指定しています。この 2 つの指定がなければ sample_app:main なんていう冗長なコマンド名になってしまいます。

そして、hello というコマンドを実装しており、bin/rails sample_app:hello と実行することを意図しています。

コマンドの実装方法の詳細については、Thor の Wiki を参照ください。

bin/rails の変更

次に独自コマンドを rails コマンドに認識させる必要があります。このため bin/rails を修正して、独自コマンドを組み込みます。次のように修正します。

bin/rails
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'

#### ↓↓↓↓↓↓↓↓追加
# 独自コマンドの組み込み
require_relative '../lib/commands/main/main_command'
#### ↑↑↑↑↑↑↑↑追加

# run rails command
require 'rails/commands'

独自コマンドを実装したファイル lib/commands/main/main_command.rbrequire_relative で読み込んでいます。独自コマンドを rails コマンドのコマンド一覧へ登録する処理が Rails::Command::Base にありますので、読み込むだけで rails コマンドに登録されます。

試しに bin/rails を実行してみます。

$ bin/rails
  ...
  routes
  runner
  sample_app:hello
  secret
  secrets:edit
  ...

多数のコマンドが出力されるので少しわかりづらいですが、rails の標準コマンドに混じって sample_app:hello と独自コマンドが表示されています。

試しに独自コマンドを実行してみます。

$ bin/rails sample_app:hello
hello

モデルの操作とサブコマンド

コンソールに文字列を表示するような単純な処理ならこのままでも問題ありませんが、Rails アプリケーションが初期化されていないので、モデルを検索したり、作成したり、削除したりすることはできません。

Rails アプリケーションの初期化方法と合わせて、モデルを作成する独自コマンドを sample_app のサブコマンドとして追加する方法をみていきます。

まず、main_command.rb を修正してサブコマンドを追加します。

lib/commands/main/main_command.rb
require "rails/command"
require_relative '../user/user_command'
require_relative '../micropost/micropost_command'

module SampleApp
  module Command
    class MainCommand < Rails::Command::Base
      namespace "sample_app"
      @command_name = "sample_app"

      subcommand "user", SampleApp::Command::UserCommand
      subcommand "micropost", SampleApp::Command::MicropostCommand
    end
  end
end

Rails アプリケーションが初期化されていないので、クラスのオートロードは効きません。require_relative を用いて明示的に user_command.rbmicropost_command.rb を読み込む必要があります。クラスを読み込んだ後、Thor の subcommand 命令で usermicropost という2つのサブコマンドを追加しています。

user サブコマンドの実体 user_command.rb は次のように実装します。

lib/commands/user/user_command.rb
module SampleApp
  module Command
    class UserCommand < Rails::Command::Base
      desc "list", "list users."
      def list
        require_application_and_environment!

        say
        say "#{'Name'.ljust(14)}  #{'Email'.ljust(32)}  Updated At"
        say "-" * 80

        User.all.each do |user|
          say "#{user.name.ljust(14)}  #{user.email.ljust(32)}  #{user.updated_at.iso8601}"
        end
      end
    end
  end
end

user_command.rb では、ユーザー一覧を表示する list というコマンドを定義しています。このコマンドは bin/rails sample_app:user list と実行することを意図しています。

list の先頭で require_application_and_environment! を呼び出し Rails アプリケーションを初期化し、続いて User をデータベースから読み込み、コンソールに出力しています。
なお、Rails アプリケーションの初期化後は、オートロードが効くようになるので、User モデルを明示的に読み込む必要はありません。

micropost サブコマンドの実体 micropost_command.rb を次のように実装します。

lib/commands/micropost/micropost_command.rb
module SampleApp
  module Command
    class MicropostCommand < Rails::Command::Base
      desc "list", "list microposts."
      def list
        require_application_and_environment!

        say
        say "#{'Name'.ljust(14)}  #{'Content'.ljust(14)}  Created At"
        say "-" * 80

        Micropost.all.each do |post|
          say "#{post.user.name.ljust(14)}  #{post.content.ljust(14)}  #{post.created_at.iso8601}"
        end
      end
    end
  end
end

ほぼ user_command.rb と同じで、こちらの方は Micropost モデルの一覧をコンソールに出力しています。

サブコマンドを追加できたら試しに実行してみます。

$ bin/rails sample_app:user list

Name            Email                             Updated At
--------------------------------------------------------------------------------
sample          sample@example.jp                 2020-12-05T05:33:11Z

Rails チュートリアルを少し進め、ユーザーを登録したら、上のように出力されます。

rails コマンドのその他の実行方法

単に rails と実行した場合も bundle exec rails などと実行した場合も bin/rails ファイルが実行されますので、bin/rails sample_app:user list に代えて bundle exec rails sample_app:user list と実行することもできます。

要検討・改善点など

  • Rails の作法にならって Rails::Command::Base を継承した ApplicationCommand というクラスを作成し、ApplicationCommand クラスを継承するようにした方が良いのかも?
  • help コマンドがなからず追加されるが、help コマンドを実行するとエラーになる。例 bin/rails sample_app:helpbin/rails sample_app:user help など。
    • 理由は標準の help コマンドが Rails エンジンしかサポートしてない。Rails アプリケーションは全く考慮されていない。
    • 改善方法としては ApplicationCommand クラスで help コマンドを独自実装するのが良いのかなと考えています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人開発】SOFT SKILLSで紹介されているタスク管理法に特化したWebアプリを作ってみた

Rails1ヶ月チャレンジ 1つ目:PomoTask (タスク管理ツール)

※Railsの勉強として、1ヶ月に1個アプリを作っています

作ったWebアプリのリンク:https://pomo-task.herokuapp.com/

はじめに

SOFT SKILLSで紹介されていた時間管理法をご存知ですか?

このタスク管理法はポモドーロテクニック、カンバンなどを融合していて、自宅での作業が効率的に行えるようになっています。

とても良い方法ですが、不満な点が1つだけありました。それがツールの使いやすさです。SOFT SKILLSではKanban Flowが紹介されていましたが、このサービスでは個人の時間管理に特化しているわけではないので、少し不満が出てきます。

  • 集団での利用を想定して作られているため、個人では使いづらい
  • ポモドーロタイマーが使いづらい
  • タスクの締め切りを把握しにくい
  • 全体のデザインは硬め

この点を解決すべく他のサービスを探していましたが、自分の好みに合うものがなかなか見つかりませんでした。なので自分で作ることに決めました。

また、前からRailsを扱えるようになりたいと思っていたということもあります。毎月新しいプロジェクトを作ると良いと聞いたので、その1つ目です。RubyもRailsも始めて1ヶ月なので至らない点が多いと思いますが、どうかご覧いただければ幸いです。

作ったもの

先日、PomoTaskというWebアプリをリリースしました。

https://pomo-task.herokuapp.com/

localhost_3000_.png

推しポイント

  • ポモドーロタイマーの使い心地
    • START, SKIP, STOPのみの簡単な操作性
    • 何回目かが分かる
    • 音とブラウザ通知で開始、終了1分前、終了をお知らせ
  • 目標
    • その日、週のポモドーロ数が分かる
    • 年、四半期、月、週の目標を確認できる
    • 週目標は常に見られる
  • 締め切りの見やすさ
  • 曜日ルーチン
    • 毎週行うタスクは自動で追加できる
  • 集中力を高めるための仕掛け
    • ポモドーロタイマーの色が変わる
    • 集中力を高めるためのコツをヘルプに掲載
  • 色、視認性
    • タスクの背景色

おわりに

最低限使えるレベルのものが作れたような気がします。とりあえず今後は、新しいWebアプリを作ったり、PomoTaskに機能を追加しようと考えています。

付けるべき機能や改善した方がいい点があれば、コメントしていただけると嬉しいです。

参考資料

  • SOFT SKILLS ソフトウェア開発者の人生マニュアル - ジョン・ソンメズ
  • 自分を操る超集中力 - メンタリストDaiGo
  • どんな仕事も「25分+5分」で結果が出る ポモドーロ・テクニック入門 - フランチェスコ・シリロ (勉強に使った参考書:現場で使える Ruby on Rails 5速習実践ガイド)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]複数モデルの検索機能

はじめに

検索機能をつけていきたいと思います。
簡易的なものですがポートフォリオ作成等の参考にしてください。

今回は検索対象をユーザーか投稿かを選べるようにする且、検索方法を完全一致・部分一致から選択できるようにしたいと思います。(前方一致・後方一致もアレンジで付け加えられるように少しだけ説明します。)
また検索結果を一覧にして新しいページに表示させます。

尚、gem ransackは使いません。

こんな人に向けて

1.検索機能の大枠を実装したい人。
2.gem ransackを使いたくない人。
3.実装方法のイメージが湧かない初学者。

1.実装をはじめる前に

まずどのように検索していくかの手順を先に説明していきます。

①検索フォームで入力内容を受け取り、コントローラに送る
②コントローラとモデルがデータベースからデータを受け取る。
③ビューに表示させる。

簡単にまとめると上記のような流れです。

またこれから実装するにあたって
if-else文
whereメソッド
の2点は基本であり重要でもあります。
分からない人は参考になるサイトがネットにたくさんあるので、そちらを見ながら進めてください。

2.検索機能の実装

2-1.コントローラーの作成

それでは実装していきたいと思います。
まずコントローラーを作成します。

コントローラ名とアクション名は分かりやすければ何でもOKです。(searchやfinder等)
今回はFindersコントローラにfinderアクションをつくります。

ターミナル
rails g controller Finders finder

これでコントローラ・アクション・ビューが自動作成されました。

2-2.ルートの設定

検索ボタンが押された際にどこのコントローラの何のアクションにリクエストが飛ぶのか設定します。
尚、finderアクションを使っているためresourcesは使用できません。

config/routes.rb
get 'finder' => "finders#finder"

ターミナルでrails routesと入力してみましょう。
finder GET /finder(.:format) finders#finderと出てくるはずです。
これでfinder_pathでリクエストされた際に、findersコントローラのfinder
アクションに飛ぶようになりました。

2-3.検索フォームの作成

アクションの中身を書く前に検索フォームから作成します。
form_withを使って入力された内容を受け取っていきます。
検索フォームを設置するページは人によって異なるので、ひとまずはapplication.html.erbに書きます。
別のページに設置したとしても後の記述は変わりません。

app/views/layouts/application.html.erb
<%= form_with url: finder_path, method: :get, local: :true do |f| %>
      <%= f.select :range, options_for_select([['User'], ['Post']]) %>
      <%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %>
      <%= f.text_field :word %>
      <%= f.submit "検索" %>
<% end %>

分解して説明します。

<%= form_with url: finder_path, method: :get, local: :true do |f| %>
    //findsコントローラのfindアクションに送る情報
<% end %>

form_with ~ do |f|は入力内容を受け取る定型文です。
最後のfの部分はformでもaでも大丈夫ですが、多くの人はfを使っています。

url: finder_pathで受け取る情報をどこに送るかを指定しています。

method: :getはgetメソッドを使うことを宣言しています。
rails routesfinder GET /finder(.:format) finders#finderとあるようにGETメソッドが指定されています。

local: :true同期通信なのでこのように書きます。

<%= f.select :range, options_for_select([['User'], ['Post']]) %>
<%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %>

f.select :range:選択されたものをrangeとしてアクションに送るようにしています。
f.selectdo |f|の記述によって変わり、do |form|とするならばform.selectとする必要があります。

options_for_select( [ ['要素1'],['要素2'] ] ):これでタブが作成されます。
要素1・要素2は文字列として扱うのでシングル(ダブル)クォーテーションで囲んでください。
options_for_select( [ ['要素1','A'], ['要素2','B'] ] )とすることも可能で、要素1とAは同じものとして扱われます。タブには最初の要素1・要素2が表示されます。
また2つの要素だけでなくもっと増やしたい場合は
( [ ['要素1'],['要素2'],['要素3'],['要素4']・・・ ] )としてください。

<%= f.text_field :word %>

入力されたものをwordとしてアクションに送るようにしています。

<%= f.submit "検索" %>

入力結果を送信します。"検索"でボタン内の文字を変えています。

検索フォームは完成です。

2-4.モデルの追記

finderアクションで使用する、searchesとwordsの引数を受け取るlooksメソッドをモデルに作成します。

app/models/user.rb
def self.looks(searches, words)
    if searches == "perfect_match"
      @user = User.where("name LIKE ?", "#{words}")
    else
      @user = User.where("name LIKE ?", "%#{words}%")
    end
end

まず条件分岐させ検索方法を変えます。
またwhereメソッドを使いデータベースから該当するのものを全て受け取り@userに保管します。

2-3で説明したように要素を増やし、条件分岐を加え、"#{words}"の部分を書き換えることで前方一致や後方一致の検索もできます。

2-5.アクションの記述

app/controllers/finders_controller.rb
  def finder
    @range = params[:range]
    search = params[:search]
    word = params[:word]
  @users = User.looks(search, word)
  end

先ほどのフォームで入力された情報をここで受け取ります。
@range = params[:range]search = params[:search]word = params[:word]:それぞれ検索フォームで選択・入力された情報を変数に代入しています。後にビューでも使うので@range@usersはインスタンス変数にしています。

@users = User.looks(search, word)

2-4で作ったlooksメソッドを使い、検索結果を@usersに代入しています。

フォームで選択された検索方法は
f.select :searchparams[:search]searchUser.looks(search, word)def self.looks(searches, words)if searches == "perfect_match"
と送られていることになります。

params[:search]searchUser.looks(search, word)の部分をまとめて

app/controllers/finders_controller.rb
def finder
    @range = params[:range]
    @users = User.looks(params[:search], params[:word])
end

とすることもできます。上記の書き方で進めていきます。

2-6.アクション内での条件分岐

2-5のままではユーザーの検索しかできません。
なのでif文を使い、ユーザーか投稿かを切り替えられるようにします。

app/controllers/finders_controller.rb
  def finder
    @range = params[:range]

    if @range == "User"
      @users = User.looks(params[:search], params[:word])
    else
      @posts = Post.looks(params[:search], params[:word])
    end
  end

分解して説明します。

if @range == "User"
  //ユーザーから探す処理(user.rbのlooksメソッドを使用)
else
  //投稿から探す処理(post.rbのlooksメソッドを使用)
end

@rangeにはUserかPostが入っています。
それをif-else文で分けて各モデルのlooksメソッドを使います。

2-5でuser.rbに作成したlooksメソッドを一部書き換えてapp/models/post.rbにも記述します。

app/models/post.rb
def self.looks(searches, words)
    if searches == "perfect_match"
      @post = Post.where("name LIKE ?", "#{words}")
    else
      @post = Post.where("name LIKE ?", "%#{words}%")
    end
end

これで検索対象も切り替えられるようになりました。

最後にビューを作成して完成です。

2-7.ビューの作成

検索結果一覧を表示するページをつくります。
既存のページに表示することもできますが、今回は新しくページを作成します。

app/views/finders/finder.html.erb
<% if @range == "User" %>
    <% @users.each do |user| %>
        <%= user.name %>    //例(ユーザーの名前を表示)
    <% end %>
<% else %>
    <% @posts.each do |post| %>
        <%= post.title %>   //例(投稿のタイトルを表示)
        <%= post.text %>    //例(投稿の本文を表示)
    <% end %>
<% end %>

分解して説明します。

<% if @range == "User" %>
    //検索対象がUserのとき、ユーザーを一覧表示
<% else %>
    //検索対象がPostのとき、投稿を一覧表示
<% end %>

アクションと同じようにif-else文で条件分岐させています。

<% @users.each do |user| %>
    <%= user.name %>    //例(ユーザーの名前を表示)
<% end %>
--------------------------------------------------
<% @posts.each do |post| %>
    <%= post.title %>   //例(投稿のタイトルを表示)
    <%= post.text %>    //例(投稿の本文を表示)
<% end %>

finderアクションでそれぞれ変数定義しましたが、@users @postsには検索に該当するデータが全て含まれています。
それをeach文で繰り返し表示させるよう指示しています。

これで完成です。

3.最後に

今回は検索機能の大枠をつくりました。これを活かして細かい部分はアレンジができます。
ぜひ試してください。

また実装方法はいろいろありますので、当記事だけでなく他の記事も参考にしてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

検索方法・検索対象を選択できる検索機能を実装する方法(ransack未使用)

はじめに

検索機能をつけていきたいと思います。
簡易的なものですがポートフォリオ作成等の参考にしてください。

今回は検索対象をユーザーか投稿かを選べるようにする且、検索方法を完全一致・部分一致から選択できるようにしたいと思います。(前方一致・後方一致もアレンジで付け加えられるように少しだけ説明します。)
また検索結果を一覧にして新しいページに表示させます。

尚、gem ransackは使いません。

こんな人に向けて

1.検索機能の大枠を実装したい人。
2.gem ransackを使いたくない人。
3.実装方法のイメージが湧かない初学者。

1.実装をはじめる前に

まずどのように検索していくかの手順を先に説明していきます。

①検索フォームで入力内容を受け取り、コントローラに送る
②コントローラとモデルがデータベースからデータを受け取る。
③ビューに表示させる。

簡単にまとめると上記のような流れです。

またこれから実装するにあたって
if-else文
whereメソッド
の2点は基本であり重要でもあります。
分からない人は参考になるサイトがネットにたくさんあるので、そちらを見ながら進めてください。

2.検索機能の実装

2-1.コントローラーの作成

それでは実装していきたいと思います。
まずコントローラーを作成します。

コントローラ名とアクション名は分かりやすければ何でもOKです。(searchやfinder等)
今回はFindersコントローラにfinderアクションをつくります。

ターミナル
rails g controller Finders finder

これでコントローラ・アクション・ビューが自動作成されました。

2-2.ルートの設定

検索ボタンが押された際にどこのコントローラの何のアクションにリクエストが飛ぶのか設定します。
尚、finderアクションを使っているためresourcesは使用できません。

config/routes.rb
get 'finder' => "finders#finder"

ターミナルでrails routesと入力してみましょう。
finder GET /finder(.:format) finders#finderと出てくるはずです。
これでfinder_pathでリクエストされた際に、findersコントローラのfinder
アクションに飛ぶようになりました。

2-3.検索フォームの作成

アクションの中身を書く前に検索フォームから作成します。
form_withを使って入力された内容を受け取っていきます。
検索フォームを設置するページは人によって異なるので、ひとまずはapplication.html.erbに書きます。
別のページに設置したとしても後の記述は変わりません。

app/views/layouts/application.html.erb
<%= form_with url: finder_path, method: :get, local: :true do |f| %>
      <%= f.select :range, options_for_select([['User'], ['Post']]) %>
      <%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %>
      <%= f.text_field :word %>
      <%= f.submit "検索" %>
<% end %>

分解して説明します。

<%= form_with url: finder_path, method: :get, local: :true do |f| %>
    //findsコントローラのfindアクションに送る情報
<% end %>

form_with ~ do |f|は入力内容を受け取る定型文です。
最後のfの部分はformでもaでも大丈夫ですが、多くの人はfを使っています。

url: finder_pathで受け取る情報をどこに送るかを指定しています。

method: :getはgetメソッドを使うことを宣言しています。
rails routesfinder GET /finder(.:format) finders#finderとあるようにGETメソッドが指定されています。

local: :true同期通信なのでこのように書きます。

<%= f.select :range, options_for_select([['User'], ['Post']]) %>
<%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %>

f.select :range:選択されたものをrangeとしてアクションに送るようにしています。
f.selectdo |f|の記述によって変わり、do |form|とするならばform.selectとする必要があります。

options_for_select( [ ['要素1'],['要素2'] ] ):これでタブが作成されます。
要素1・要素2は文字列として扱うのでシングル(ダブル)クォーテーションで囲んでください。
options_for_select( [ ['要素1','A'], ['要素2','B'] ] )とすることも可能で、要素1とAは同じものとして扱われます。タブには最初の要素1・要素2が表示されます。
また2つの要素だけでなくもっと増やしたい場合は
( [ ['要素1'],['要素2'],['要素3'],['要素4']・・・ ] )としてください。

<%= f.text_field :word %>

入力されたものをwordとしてアクションに送るようにしています。

<%= f.submit "検索" %>

入力結果を送信します。"検索"でボタン内の文字を変えています。

検索フォームは完成です。

2-4.モデルの追記

finderアクションで使用する、searchesとwordsの引数を受け取るlooksメソッドをモデルに作成します。

app/models/user.rb
def self.looks(searches, words)
    if searches == "perfect_match"
      @user = User.where("name LIKE ?", "#{words}")
    else
      @user = User.where("name LIKE ?", "%#{words}%")
    end
end

まず条件分岐させ検索方法を変えます。
またwhereメソッドを使いデータベースから該当するのものを全て受け取り@userに保管します。

2-3で説明したように要素を増やし、条件分岐を加え、"#{words}"の部分を書き換えることで前方一致や後方一致の検索もできます。

2-5.アクションの記述

app/controllers/finders_controller.rb
  def finder
    @range = params[:range]
    search = params[:search]
    word = params[:word]
  @users = User.looks(search, word)
  end

先ほどのフォームで入力された情報をここで受け取ります。
@range = params[:range]search = params[:search]word = params[:word]:それぞれ検索フォームで選択・入力された情報を変数に代入しています。後にビューでも使うので@range@usersはインスタンス変数にしています。

@users = User.looks(search, word)

2-4で作ったlooksメソッドを使い、検索結果を@usersに代入しています。

フォームで選択された検索方法は
f.select :searchparams[:search]searchUser.looks(search, word)def self.looks(searches, words)if searches == "perfect_match"
と送られていることになります。

params[:search]searchUser.looks(search, word)の部分をまとめて

app/controllers/finders_controller.rb
def finder
    @range = params[:range]
    @users = User.looks(params[:search], params[:word])
end

とすることもできます。上記の書き方で進めていきます。

2-6.アクション内での条件分岐

2-5のままではユーザーの検索しかできません。
なのでif文を使い、ユーザーか投稿かを切り替えられるようにします。

app/controllers/finders_controller.rb
  def finder
    @range = params[:range]

    if @range == "User"
      @users = User.looks(params[:search], params[:word])
    else
      @posts = Post.looks(params[:search], params[:word])
    end
  end

分解して説明します。

if @range == "User"
  //ユーザーから探す処理(user.rbのlooksメソッドを使用)
else
  //投稿から探す処理(post.rbのlooksメソッドを使用)
end

@rangeにはUserかPostが入っています。
それをif-else文で分けて各モデルのlooksメソッドを使います。

2-5でuser.rbに作成したlooksメソッドを一部書き換えてapp/models/post.rbにも記述します。

app/models/post.rb
def self.looks(searches, words)
    if searches == "perfect_match"
      @post = Post.where("name LIKE ?", "#{words}")
    else
      @post = Post.where("name LIKE ?", "%#{words}%")
    end
end

これで検索対象も切り替えられるようになりました。

最後にビューを作成して完成です。

2-7.ビューの作成

検索結果一覧を表示するページをつくります。
既存のページに表示することもできますが、今回は新しくページを作成します。

app/views/finders/finder.html.erb
<% if @range == "User" %>
    <% @users.each do |user| %>
        <%= user.name %>    //例(ユーザーの名前を表示)
    <% end %>
<% else %>
    <% @posts.each do |post| %>
        <%= post.title %>   //例(投稿のタイトルを表示)
        <%= post.text %>    //例(投稿の本文を表示)
    <% end %>
<% end %>

分解して説明します。

<% if @range == "User" %>
    //検索対象がUserのとき、ユーザーを一覧表示
<% else %>
    //検索対象がPostのとき、投稿を一覧表示
<% end %>

アクションと同じようにif-else文で条件分岐させています。

<% @users.each do |user| %>
    <%= user.name %>    //例(ユーザーの名前を表示)
<% end %>
--------------------------------------------------
<% @posts.each do |post| %>
    <%= post.title %>   //例(投稿のタイトルを表示)
    <%= post.text %>    //例(投稿の本文を表示)
<% end %>

finderアクションでそれぞれ変数定義しましたが、@users @postsには検索に該当するデータが全て含まれています。
それをeach文で繰り返し表示させるよう指示しています。

これで完成です。

3.最後に

今回は検索機能の大枠をつくりました。これを活かして細かい部分はアレンジができます。
ぜひ試してください。

また実装方法はいろいろありますので、当記事だけでなく他の記事も参考にしてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gem 'Gimei'

Gimeiとは

日本人の名前やフリガナ、住所などを自動生成してくれるGem。
有名なGemでFakerがあるが、Fakerでは対応できないフリガナを使うことができる。
Gimei

使い方

開発環境とテスト環境で利用するのでgroup :development, :test doの内部でgemを指定、Gemfileを編集したらアプリケーションのディレクトリでbundle installを実行。

Gemfile
group :development, :test do
  # 省略
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'faker'
  gem 'gimei'
end

コンソールで実行するとこんな感じで名前が生成される。

console
[1] pry(main)> japanese_user = Gimei.name
# 省略
[2] pry(main)> japanese_user.last.kanji
=> "島村"
[3] pry(main)> japanese_user.last.katakana
=> "シマムラ"

FactoryBotと組み合わせて架空のユーザーを生成する。
インスタンスを生成せずにGimei.name.first.kanjiなどを入れると、名前とフリガナが一致しなくなる。

factories/users.rb
FactoryBot.define do
  factory :user do
    # インスタンスを生成
    japanese_user = Gimei.name

    first_name { japanese_user.first.kanji }
    first_name_kana { japanese_user.first.katakana }
    last_name { japanese_user.last.kanji }
    last_name_kana { japanese_user.last.katakana }
  end
end
console
[1] pry(main)> FactoryBot.create(:user)
# 以下実行結果が表示される。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】表の合計値算出方法(aggregate関数)

はじめに

Railsで表の合計値を算出するにあたって、aggregate関数なるものが便利でスマートだったので、記事にしてみました。

開発環境

IDE:Cloud9
Ruby:2.6.3
Rails:5.2.4

実例をみてみる

やりたいこと

このような表を作成し、下に合計値を表示したい。
_2020-11-15_17.21.26.png

ER図

_2020-12-08_14.20.07.png

aggregate関数を使わない場合

controllers/carts.rb
def new
  @carts = Cart.where(user_id: current_user.id)
end

例えば、たんぱく質(protain)の合計値を表示したい場合

views/carts/new.html.rb
<% sum = 0 %>
<% @carts.each do |cart| %>
<% sum += cart.food.protain %>
<% end %>
<%= sum %>

これだと、1つの項目(上記の場合たんぱく質)を表示するのに、5行も必要となってしまい、見ため的にあまりスマートとはいえない。

views/carts/new.html.rb
<td>合計</td>
<td>
    <% sum = 0 %>
    <% @carts.each do |cart| %>
    <% sum += cart.food.calorie %>
    <% end %>
    <%= sum %>
</td>
<td>
    <% sum = 0 %>
    <% @carts.each do |cart| %>
    <% sum += cart.food.protain %>
    <% end %>
    <%= sum %>
</td>
<td>
  <% sum = 0 %>
    <% @carts.each do |cart| %>
    <% sum += cart.food.fat %>
    <% end %>
    <%= sum %>
</td>
<td>
  <% sum = 0 %>
    <% @carts.each do |cart| %>
    <% sum += cart.food.carbon %>
    <% end %>
    <%= sum %>
</td>

表全体を表示すると、割とfat感がある。
そこでaggregate関数を用いて、もっとスマートに記述する。

aggregate関数を使う方法

コントローラはさっきと一緒。

controllers/carts.rb
def new
  @carts = Cart.where(user_id: current_user.id)
end

カートモデルに以下を記述する。

model/cart.rb
def self.aggregate(column)
  self.all.map { |cart| cart.food[column] }.sum
end
views/carts/new.html.rb
<td>合計</td>
<td>
  <%= @carts.aggregate(:calorie) %>
</td>
<td>
  <%= @carts.aggregate(:protain) %>
</td>
<td>
  <%= @carts.aggregate(:fat) %>
</td>
<td>
  <%= @carts.aggregate(:carbon) %>
</td>

めっちゃすまーと。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails database.ymlの記載内容の適用のされ方に関するメモ

記事の目的

データベースの接続の設定ファイルであるdatabase.ymlの記載内容の適用のされ方に関するメモ

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV["DB_USERNAME"] %>
  password: <%= ENV["DB_PASSWORD"] %>
  host: localhost

development:
  <<: *default
  database: book_action_development


test:
  <<: *default
  database: book_action_test


production:
  <<: *default
  database: book_action_production
  username: root
  password: <%= Rails.application.credentials.db[:password] %>
  host: <%= Rails.application.credentials.db[:hostname] %>

勘違い

あるエラーが起きて、
default部分に記載のusername: <%= ENV["DB_USERNAME"] %>と
productionに記載のusername: root
が競合してしまうと思っていた。

実際

競合はせず、productionに記載のusername: rootが優先されるそう。

まとめ

なぜそうなるかは詳しく調べられていないので、今後調べる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Basic認証 Part 1

始めに

今回は、RailsアプリケーションにBasic認証を導入する方法について説明します。練習としてデプロイしている自身のアプリに対してサーバーの負荷を増やしたくないときなどは、Basic認証を導入して閲覧できるユーザーを制限しましょう。

Basic認証

Basic認証とは、HTTP通信の規格に備え付けられているユーザー認証の仕組みです。
サーバーと通信が可能なユーザーとパスワードをあらかじめ設定しておき、それを知っているユーザーのみがWebアプリーションを利用できるようにすることができます。

Qiita

Ruby on Railsには、Basic認証を導入するためのメソッドが用意されており、簡単に実装することができます。
便利な世の中ですね(笑)

authenticate_or_request_with_http_basic

Ruby on RailsでBasic認証を実装するためのメソッドです。
ブロックを開き、ブロック内部でusernameとpasswordを設定することでBasic認証を利用できます。

サンプル
# 'admin'というユーザー名と、'password'というパスワードでBasic認証できるように設定
authenticate_or_request_with_http_basic do |username, password|
  username == 'admin' && password == 'password'
end

Basic認証のRailsアプリケーションへの導入

authenticate_or_request_with_http_basicメソッドを利用して、開発中のRailsアプリケーションにBasic認証を導入しましょう。

RailsアプリケーションにBasic認証を導入しよう

Basic認証によるログインの要求は、全てのコントローラで行いたいです。そこで、Basic認証の処理をapplication_controller.rbにて、private以下にメソッドとして定義し、before_actionで呼び出すように実装しましょう。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :basic_auth

  private

  def basic_auth
    authenticate_or_request_with_http_basic do |username, password|
      username == 'admin' && password == '2222'
    end
  end
end

これで、どのページにアクセスしてもBasic認証が要求されるようになりました。今回は、adminというユーザー名と2222というパスワードでBasic認証ができるように設定しています。

Basic認証の動作を確認しよう

application_controller.rbの記述が完了したら、実際にアプリケーションを起動し、Basic認証の動作が期待したものになっているか確認してみましょう。

'rails s'をした上で、適当なページにアクセスすると、ユーザー名とパスワードの入力を求めるポップアップウインドウが表示されます。間違ったユーザー名とパスワードではBasic認証できないことを確かめるために、あえて適当な文字を入力してみましょう。以下のGifでは、ユーザー名にhogehoge、パスワードに1111と入力しています。

Qiita

正しく実装できていれば、もう一度Basic認証用のポップアップウインドウが表示されます。 間違った情報ではログインできないことを確認できたので、今度は正しい情報を入力してログインできるかどうかを確かめてみましょう。ユーザー名にadmin、パスワードに2222と入力して、もう一度ログインを試みてください。

Qiita

ルーティングに対応したページが表示されれば、Basic認証に成功しています。

ひとまず長いので、Part2続きます。
ここまでで、問題点が2つほどあるので考えてみてくだい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CSRFについて

はじめに

Webアプリケーションには、利用者自らの操作では取り消しのできない重要な処理があります。ここでいう「重要な処理」とはECサイトでのクレジットカードでの決済やメールの送信、パスワードの変更などのことをいいます。
こういった重要な処理の過程で不備があると、クロスサイトリクエストフォージェリ(以下CSRFと省略)の脆弱性が生じる場合があります。CSRFは「シーサーフ」もしくは「シーエスアールエフ」と呼ばれます。

CSRF(クロスサイトリクエストフォージェリ)

本章では掲示板への投稿やメールの送信、ECサイトでの商品購入など本来外部より実行されてはいけない重要な処理を、なんらかの攻撃手法を用いて実行させてしまうという攻撃手法であるCSRF(クロスサイトリクエストフォージェリ)の概要と対策方法を学習していきます。

CSRFとは

Webサイトにスクリプトや自動転送(HTTPリダイレクト)を仕込むことによって、利用者に意図せず別のWebサイト上で何らかの操作(掲示板への書き込みや銀行口座への送金など)を行わせる攻撃手法のことをいいます。
CSRFの脆弱性が存在すると以下のような被害を被る可能性があります。

利用者のアカウントによる物品の購入
利用者の退会処理
利用者のアカウントによる掲示板への書き込み
利用者のパスワードやメールアドレスが変更
CSRF脆弱性の影響は「重要な処理」の悪用に限られるため、CSRFの脆弱性を個人情報の取得等に用いることはできません。

CSRFの攻撃例

利用者が罠サイトを閲覧することによってパスワードが変更されてしまう例

利用者がexample.jpにログインしている
攻撃者は罠を作成
利用者が罠を閲覧する
罠のJavaScriptによる、被害者のブラウザ上で攻撃対象サイトに対し、新しいパスワードabcdefがPOSTメソッドにより送信される
パスワードが変更される

Qiita

CSRFの対策

一般的な対策方法

CSRF攻撃を防ぐには、「重要な処理」に対するリクエストが利用者の意図によるものかどうかを確認することが必要
このためCSRF対策としては、以下の2点が考えられる。

CSRF対策の必要なページを区別する
正規利用者の意図したリクエストを区別できるように実装する

CSRF対策の必要なページを区別する

CSRF対策はすべてのページに行う必要はありません。むしろ、対策の必要がないページの方が多いのです。対策に必要なページは、他のサイトから勝手に実行されては困るようなページです。例えば、ECサイトの物品購入ページや、パスワード変更など個人情報の編集確定画面などが挙げられます。
CSRFの対策としては、まず実装するWebアプリケーションのどのページに脆弱性対策が必要なのか設計段階で明らかにすることです。
以下の図のように、機能一覧の中でCSRFの対策が必要なページを色分けすると良いと思います。

Qiita

正規利用者の意図したリクエストを区別できるように実装する

CSRF対策で必要なことは、正規利用者の意図したリクエストなのかどうかということです。
ここでの意図したリクエストとは、利用者が対象のアプリケーション上で「実行」ボタンなどを押して、「重要な処理」のリクエストを発行することをいいます。
正規のリクエストかどうかを判断する方法は3種類あります。

秘密情報の埋め込み
・パスワードの再入力
・Refererのチェック
・Refererによるチェックは少し難しいので、上の二つについて説明します。

秘密情報の埋め込み

登録画面や注文確定画面などのCSRF攻撃への対策が必要なページに対して、第三者の不正利用者が知り得ない秘密情報を要求するようにすれば、不正リクエストによる重要な処理が実行されることはありません。このような目的で使用される秘密情報のことをトークンといいます。

Qiita

パスワードの再入力

こちらは文字通り重要な処理が確定する前に、再度パスワードを入力してもらいます。これはCSRF対策の他にも物品の購入などに先立って、利用者の意思の念押しをしたり、共用のPCにおいて正規の利用者以外の利用者が、重要な処理を実行するのを防いだりする効果があります。
CSRFの攻撃例として、とりあげたパスワード変更ページにも現在のパスワードを再入力させることによりCSRF攻撃を防ぐことが可能です。
しかし、対策が必要なページ以外でパスワードの再入力を求めるページが複数あると煩雑なアプリケーションになってしまうため、注意が必要です。

RailsでのCSRF対策方法

それではRailsではどのようにCSRF対策を行っていけばいいのでしょうか。
実は、Rails側できちんと対策を行ってくれています。基本的には開発者はなにもしなくても大丈夫です。
それでは実際にRailsがどのようにCSRF対策を行っているか見ていきましょう。

ChatSpaceのapplication_controller.rbを開いてみましょう。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# 省略
  protect_from_forgery with: :exception
# 省略
end

protect_from_forgery with: :exceptionという記述があるかと思います。これがRailsアプリケーション内でCSRF対策を行うというという命令になります。
こちらをすべてのコントローラの親であるapplication_controller.rbに記述することによって、その子コントローラすべてに先ほど説明したようなCSRF対策をRails側で行ってくれます。
なおRails5.2以降では、ActionController :: Baseで、はじめからCSRF対策機能が有効になっているので、application_controller.rbにこの記述がなくなります。

CSRF対策としてどのような事が行われているかというと、まずサイトのHTMLに一意のトークンを埋め込みます。これと同じトークンを、セッションcookie(クッキー)にも保存しています。ユーザーがPOSTリクエストを送信すると、HTMLに埋められているCSRFトークンも一緒に送信されます。あとは、サーバ側でページのトークンとセッション内のトークンを比較し、両者が一致することを確認したらリクエストを受け付けます。

まとめ

CSRFとはアプリケーション内で重要な処理が行われる場所で発生する攻撃手法である
CSRFを防ぐにはトークンを埋め込んだり、パスワードを再入力させたり、ページ遷移の際正規利用者しか知り得ない情報を用いることが有効
RailsでCSRF対策を行う際はコントローラ内にprotect_from_forgeryメソッドを記述する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AmazonLinux2 + Ruby on Rails6でSQLite古いエラー

エンタメ系企業の社内もろもろを担当しているakibinです。

Twitterアカウント @AkibinMusic
Youtubeチャンネル

内容

AmazonLinux2を立てて、Railsをインストールしたけどアプリ起動したら以下がエラーが出た場合の対処方法を書き書きします。

【エラー】
Your version of SQLite (3.7.17) is too old. Active Record supports SQLite >= 3.8.

なぜエラーが出るのか

AmazonLinux2にはsqlite3のバージョン3.7がデフォルトでインストールされており、これがyumで利用しているらしく、削除することはできません。がしかし、Rails 6を動かすには、sqlite3のバージョン3.8以上が必要なので、古い!と↑で怒ってるんですね。

対処方法

既存のSQLiteはほっておいて、3.8以上のSQLiteを別途インストールします。

# アプリ用のユーザのホームディレクトリで作業(私は)
$ cd /home/appuser 
# 3.34.0をダウンロード
$ wget https://www.sqlite.org/2020/sqlite-autoconf-3340000.tar.gz
$ tar xzvf sqlite-autoconf-3340000.tar.gz
$ cd sqlite-autoconf-3340000
# もとから入っているsqliteと競合しないように /opt/sqlite/sqlite3にインストールする
$ ./configure --prefix=/opt/sqlite/sqlite3
$ make
$ sudo make install
# バージョン確認
$ /opt/sqlite/sqlite3/bin/sqlite3 --version
3.34.0 2020-12-01 16:14:00 a26b6597e3ae272231b96f9982c3bcc17ddec2f2b6eb4df06a224b91089fed5b

アプリで使用するSQLiteとして、今回インストールしたSQLiteをgemで指定する。

# アプリ用のディレクトリに移動
$ cd /var/www/rails/webapp 
# gemで新しいSQLiteを指定
$ gem install sqlite3 -- --with-sqlite3-include=/opt/sqlite/sqlite3/include \
   --with-sqlite3-lib=/opt/sqlite/sqlite3/lib
# 指定が正しくできているか確認
$ irb
irb(main):001:0> require 'sqlite3'
=> true
irb(main):002:0> SQLite3::SQLITE_VERSION
=> "3.34.0"  # できてる
irb(main):003:0> exit

あと新しいSQLiteにパスを通す必要があるので以下作業実施。

$ echo 'export LD_LIBRARY_PATH="/opt/sqlite/sqlite3/lib"' >> .bash_profile
$ source .bash_profile

これで再度アプリ起動するとエラー解消して以下が出ました!

こちらの記事参照してSQLite部分のみ抜粋させていただきました!わかりやすかったです!

今年も終わるは早いわ…

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

master.keyが無い問題の解決への道 [config/master.key: No such file or directory]

最初に

カレンダー企画2020の8日目
プログラミングの勉強を始めて3ヵ月程経ったので学んだことのメモをアウトプットとして記事に残します。
これからプログラミングの世界に入る人の手助けになれたら嬉しい限りです。
間違っていたり、言葉が違っていたり、誤解されるような言葉があったら教えてください^^
言葉を長々と読みづらかったら申し訳ありません。少しずつなれてがんばります。

deployをしようとした時にmaster.keyがなくて困った

こんなのがエラーで出てくる^^;

ターミナル
config/master.key: No such file or directory

環境

Rails 5.2.4

無くなった原因

git push時にmaster.keyは除外されるようになっているのでGitHub上には残らない(GitHubにあがるとまずい^^;)
git cloneを行ったりするとmaster.keyがない状態になる。(私はこれが原因でした!)
他にもあるのかもですが分からないです!

解決

  1. アプリケーションのディレクトリに移動
  2. config/credentials.yml.encというファイルがあると思う。それを削除します
  3. そうするとconfigの中にはmaster.keycredentials.yml.encがない状態になる
  4. これをターミナルで実行
ターミナル
sudo EDITOR="vi" rails credentials:edit
  1. するとconfigの中にmaster.keycredentials.yml.enc現れる。

もし、このあとdeployをする場合はpushをしてdeploy環境下でpullを忘れずに!
credentials.yml.encの中身が変わっているのでこれをしないとまた違うエラーが出るよ!(経験済!^^)

そもそもmaster.keycredentials.yml.encって何??

credentials.yml.encが暗号化されているよう…
master.keyを使って復号化するみたい…

2つはセットでどちらかが欠けてもダメってことですね!(調べて見て鍵と鍵穴の関係で説明されていました!)
とくにmaster.keyは大事なので保管方法にも気をつけた方が良いみたいです!

これを見て勉強しました!(良かったらどうぞ)
参考サイト:https://techtechmedia.com/credentials-masterkey-rails/#masterkey-2

最後に

自分が詰まったエラーだったのでメモのつもりで残しました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[解決](Rails, Vue)CORSエラーAccess to XMLHttpRequest at 'http://localhost:3000' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

なぜこのエラーが出るのか。

https://qiita.com/att55/items/2154a8aad8bf1409db2b

解決方法(Rails, Vue)

railsのgem

gemfile
#gem rack-coes

のコメントアウトを外し、

$bundle install


config/initializers/cors.rbにある記述も下のようにコメントアウト。

config/initializers/cors.rb
# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:3000', 'https://localhost:8080/'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

originsは、サーバーサイド(API),フロントの順で書きます。

Dockerの場合は、再度コンテナを立ち上げて

完了。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gemのバージョン指定について

railsアプリケーションを作成するにあたって
gemファイル内のバージョン指定について理解が不十分だったので整理してみました。

Gemfileとは

・これは何?
 Bundlerというrubyのライブラリ管理システムのファイル。
・何ができる?
 railsアプリで使用するライブラリを管理することができる。

バージョンの基本の書き方

Gemfile
gem 'gem名', 'バージョン', 'オプション'

x.y.zの表記の意味

x:メジャーバージョン
 重大な変更。新機能の追加や、多くのAPI変更が含まれる。
y:マイナーバージョン
 新機能の追加やAPIの追加が含まれることがある。
z:パッチバージョン
 バグの修正が含まれる。

バージョン指定の指定について

固定

'x.y.z'

Gemfile
gem 'sqlite3', '1.3.6'

〜以上

'>=x.y.z'

Gemfile
gem 'sqlite3', '>=1.3.6'

x.x.x以上、x.y+1.0未満(メジャーアップデート不可)

'~>x.y.z'

Gemfile
gem 'sqlite3', '~>1.3.6'

以下と同義ですね。

Gemfile
gem 'sqlite3', '>=1.3.6', '<1.4.0'

x.y.z以降で最新のもの

'>=x.x.x'

Gemfile
gem 'sqlite3', '>=0.8.5'

参考

https://blog.yuhiisk.com/archive/2017/04/24/specify-the-version-of-gemfile.html
https://haayaaa.hatenablog.com/entry/2018/10/29/235952

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初めてのwebアプリ実装

某プログラミングスクールの実装課題を完了したので、記録を残しておきます。
なお、私がかけた時間はトータル24時間強でした。スクールの定める予定時間を大幅に上回っていたため、恐らくかなりエラーに苦しめられた方だと思います。。

なぜ記録を残すのか

将来の私のために、初めてアプリを作った時の新鮮な感情を残しておきたいため。
また、同じように初めての実装で悩んでいて、挫折しそうな人に、考え方の参考として見ていただきたいため。

実装を通しての学び

実装を通しての学びは以下の3つです。

  1. アウトプットの重要性
  2. プログラミングの勉強においてエラー対処は利点しかない
  3. エラーの解決手順

1.アウトプットの重要性

これはどんな学問に対しても当てはまることです。ただ、学生の頃の試験勉強と比較すると、プログラミングの勉強の方が、よりアウトプットは重要だと思いました。

理由としては、答えは一つではないものの、答えを導き出すためには明確な論理がある、からです。

プログラミングにおいて、答えは一つではなく、明確な論理を持って実装に当たる必要があります。
答えは非常に多くのパターンがあり、実装のためには様々なことを頭の中から出せる状態にしておける必要があると思います。また、勉強したことを実行すれば100点が必ず取れるような物でもありません。様々な要因からエラーが必ずおきます。

勉強したことをアウトプットできる状態にして、さらに予測不能なエラーに対処できる力を身に付ける。これがプログラミングにおけるアウトプットの意義だと思います。

2.プログラミングの勉強においてエラー対処は利点しかない

これはどんな人にとっても間違いないことだと思います。
なぜなら、エラーでは、不備の箇所を明確にしてくれるからです。
例えば、数学の問題を間違えたとして、どこが分かっていないかは、自分で考える必要があります。反対にプログラミングは、どこが間違っているかを明確にしてくれて(エラー文が表示されている場合)、どのように修正するべきかも提案してくれたりします(Did you mean)。
エラーってポジティブ要素に聞こえてきませんか?笑

3.エラーの解決手順

MVC実装程度のアプリ開発におけるエラー解決手順ですが、私が実際にエラー対処する中での解決手順です。
1)エラー文の解析
 簡単なエラー文やDid you mean?が出ていれば、即解決です。複雑なエラー文であっても、検索すれば、簡単に解決できることも。
2)エラー文で指摘されているファイルのチェック
 スペルミスや構文ミス(特にend抜け)がないかをチェック
3)MVCの流れをチェック
 1),2)で解決しない場合は、 MVCの処理がおかしくなっているはずなので、流れをチェックします
4)分かっている人に質問する
 1)~3)を実施して不可能な場合は大人しく諦めましょう。ただし、質問の仕方としては、参考資料を持って、こうしようとしたけど、思い通りにいかなかった、などしっかり考えた証を見てもらいながらだといいと思います。

1)〜3)については時間をかければかけるほど、4)での定着率は高まります。私はdevise導入時に、マイグレーションの
記述を消しすぎていることに気づかず、6時間以上ストップしていました。もう一生userモデルのマイグレーションファイルの記述をおざなりにすることはないと思います。

まとめ

グダグダと書いてきましたが、結論、僕が伝えたいことは、エラー発生はポジティブ要素だということです。
エラーにかける時間が長ければ長いほど、特にだと思います。
実装の達成感はとてつもないです。とにかくやり切ることが成長につながると思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DeviseのUserテーブルにUpdateアクションでカラムを更新しようとするとうまくいかない現象について

事の発端

Deviseで作ったUserモデルのテーブルにカラムを更新しようとするとできなかったことが始まりです。

意外なところで詰まったなぁと思ったので健忘録としてまとめます。

user_controller.rb
  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      redirect_to user_path(@user.id)
    else  
      render :show
    end
  end

僕は月の走行距離をマイページにて追加したかった為詳細ページにform_withを構えています。

show.html.erb
  <%= form_with(model: @user, local: true, class: "goal-form")  do |f| %>
    <%= f.text_field :distance, placeholder: "目標を記入する", class: "form__text" %>
    <%= f.submit "設定する", class: "btn btn-primary" %>
  <% end %>

送られてくる値も間違ってなかったのでなんでやねんと思っていたとこでした。
そこで以下のエラーを見つけました。
EFE24E99-97AA-473B-8332-1DEF840AD209_4_5005_c.jpeg

そもそもUserテーブルの編集にはPasswordの入力が必要だということ。

知りませんでした。
これまでユーザーの編集を行うことがなかったんです。

そこでPasswordを入力せずにユーザーの編集を行う方法を見つけました。
カラムを更新するには新たなコントローラの作成やメソッドが必要なようです。

まずは、users/registrations_controller.rbを作成します。

registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_account_update_params, only: [:update]

  protected

  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end
end

次にルーティングを設定します。

route.rb
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

Userモデルを編集。メソッドを追加します。

user.rb
  メソッドを追加
  def update_without_current_password(params, *options)
    params.delete(:current_password)

    if params[:password].blank? && params[:password_confirmation].blank?
      params.delete(:password)
      params.delete(:password_confirmation)
    end

    result = update_attributes(params, *options)
    clean_up_passwords
    result
  end
end

Userモデルで定義したメソッドを呼び出します。

registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_account_update_params, only: [:update]

  protected
追加
  def update_resource(resource, params)
    resource.update_without_password(params)
  end

  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:distance])
  end

また、モデルにpasswordのバリデーションがかかっているとまだエラーになると思うので外しておきましょう。

user.rb
  with_options presence: true do
    validates :nickname
    validates :email
    validates :password    ←消す
  end  

https://gyazo.com/d2738db841e41a0679d16fae6836ace9

できました。ほぼコピペです…

ここで初見だった方々が多かったのでまとめてみました。

・:account_update・・・Updateをするときに指定する引数。
・blank?・・・中身が空もしくは存在しないときにtrueを返す。
・update_attributes・・・一つのカラムのみを変更できる。しかし、バリデーションがスルーされる為、エラーの判定位が出ない。(これに苦しめられた)

キリがないのでここまで。

完成

これから、jsで今日走った距離が引かれて減っていくような機能を導入したいなと思います。
お疲れ様でした。

参考文献

https://qiita.com/j-sunaga/items/8d6769dfd04da5d3eed5

https://qiita.com/somewhatgood@github/items/b74107480ee3821784e6

https://pikawaka.com/rails/update

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【メモ】backgroundの使い方

background

背景(画像)は以下の設定で行う

sample.html.erb
<body>
  <div class="top_wallpaper">
|
|
|
sample.css
.top_wallpaper{
  background-image: url(/wallpaper-new.jpg);
}

【CSS】
・該当するクラスに対し"background-image"を使用

書き方

background-image: url(/xxx.yy)
 ・/:フォルダの場所("/"のみの場合publicフォルダが自動的に適用)
 ・xxx:ファイル名
 ・yy:拡張子名

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ensure は実行されるとは限らない

この記事は、Ruby Advent Calendar 2020 の 8 日目の記事です。
昨日は @udzura さんの mruby 本を支えた Continuous Delivery でした。

概要

Thread#raise を使う場合に ensure が実行されないケースがあります。
代表例としては Timeout.timeout があります。

Rails をはじめとするライブラリはクリーンアップ処理を ensure に頼っているケースが多いので、リクエスト中に Timeout.timeout した場合、そのプロセスは後続リクエストを正常に処理できない可能性があります。
次のリクエストを処理する前に終了させたほうがよいです。

rack middleware やライブラリ内でこれらを使用していないかには注意したほうがよいでしょう。
例えば、rack-timeout を使ってデフォルト設定のままの場合には、必ず term_on_timeout: true にしましょう。

詳細

次のコードを実行してみてください。
ensure できなかった場合にのみ無限ループから抜けます。

require 'timeout'

class Foo
  def stack
    @stack ||= []
  end

  def foo
    stack.push('hi')
  ensure
    stack.pop
  end
end

foo = nil
loop do
  foo = Foo.new
  begin
    Timeout.timeout(Float::EPSILON) do
      loop do
        foo.foo
      end
    end
  rescue Timeout::Error
    break unless foo.stack.empty?
  end
end

p foo.stack
# => ["hi"]

まさにこのような場合でも確実に ensure するために Thread.handle_interrupt があります。

https://docs.ruby-lang.org/ja/master/method/Thread/s/handle_interrupt.html

require 'timeout'

class Foo
  def stack
    @stack ||= []
  end

  def foo
    Thread.handle_interrupt(Timeout::Error => :never) do
      begin
        stack.push('hi')
      ensure
        stack.pop
      end
    end
  end
end

foo = nil
loop do
  foo = Foo.new
  begin
    Timeout.timeout(Float::EPSILON) do
      loop do
        foo.foo
      end
    end
  rescue Timeout::Error
    break unless foo.stack.empty?
  end
end

p foo.stack
# ここまでこない

つまり、ensure の位置でこの Thread.handle_interrupt をアプリケーションコードとライブラリのコードすべてに書いて回る必要があります。もっと言うと、すべての行でいつでも例外が上がってくるかもしれないということなので、そのようなことが起きても内部状態が壊れないよう考慮したコーディングが必要になります。

これについて、Rails ではきりがないため考慮しないこととしています。

https://github.com/rails/rails/pull/17607#issuecomment-70538060

ところで、ActiveRecord は scope 周りで ensure を利用している部分があります。

https://github.com/rails/rails/blob/9b6008924d527b61b11677a78542a7b0fd4d80bf/activerecord/lib/active_record/relation.rb#L812-L813

これを利用して、Timeout 前のリクエストで投げようとしていたクエリの条件を Timeout 後のスコープに入れ込むことが可能になります。

つまり、

User.count

というコードを、

User.where(name: "foo").count

に化けさせることができます。

以下が再現させるコードの例です。
保存して $ ruby foo.rb のようにすればそのまま実行できます。

require "bundler/inline"

gemfile do
  source "https://rubygems.org"

  gem "rails", "6.0.3"
  gem "sqlite3"
  gem "rack-timeout", require: "rack/timeout/base"
end

require "rack/test"
require "active_record/railtie"
require "action_controller/railtie"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(File::NULL)
ActiveRecord::Base.logger.level = Logger::ERROR

ActiveRecord::Schema.define do
  create_table :users, force: true do |t|
    t.string :name
  end
end

class User < ActiveRecord::Base
  scope :sleep_a_while, -> { all.tap { sleep(rand / 1000000.0) } }
end

User.create!(name: "foo")
User.create!(name: "bar")

class TestApp < Rails::Application
  config.root = __dir__
  config.hosts << "example.org"
  config.session_store :cookie_store, key: "cookie_store_key"
  secrets.secret_key_base = "secret_key_base"

  config.logger = ActiveRecord::Base.logger
  Rails.logger  = config.logger

  config.middleware.insert_before Rack::Runtime, Rack::Timeout, service_timeout: 0.1

  config.exceptions_app = ->(*) { [500, {}, ['']] }

  routes.draw do
    get "/" => "test#index"
    get "/timeout" => "test#timeout"
  end
end

class TestController < ActionController::Base
  include Rails.application.routes.url_helpers

  def index
    render plain: User.count
  end

  def timeout
    loop do
      User.where(name: "foo").sleep_a_while
    end
  end
end

require "minitest/autorun"

class BugTest < Minitest::Test
  include Rack::Test::Methods

  def test_index
    get "/"

    # User.count が 2 であることを確認
    assert_equal "2", last_response.body

    until last_response.body == "1"
      get "/timeout"

      get "/"
    end

    get "/"
    # User.count の結果は常に 2 でないといけないが、1 になっているためテストは失敗する
    assert_equal "2", last_response.body
  end

  private
    def app
      Rails.application
    end
end

このように、ensure に頼って書かれているコード全般が Timeout.timeout と組み合わせることで壊れる可能性があります。

その他

8年前に ko1 さんが ensureTimeout.timeout の組み合わせの危険性についてアドベントカレンダーで書かれていました。
http://atdot.net/~ko1/diary/edit_comment.cgi?mode=long&year=2012&month=12&day=6

また、rack-timeout ではこういった timeout のリスクについて記しています。
https://github.com/sharpstone/rack-timeout/blob/f4b14a534b37d425ec4dba10d9cacf29ba012f9f/doc/risks.md

とはいえ、rack-timeout を使うにあたってはデフォルトで term_on_timeout: true のほうが望ましいと思われるので issue を立てています。
https://github.com/sharpstone/rack-timeout/issues/169

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む