20191203のRailsに関する記事は20件です。

Google Compute Engine&Nginx&Railsタイムアウト考察

diffeasy Advent Calendar 2019の3日目の記事です。

タイムアウト対策

Webでの大量データのPDF、CSVダウンロードなど、API実行すると処理が圧倒的に長くなるとタイムアウトになります。根本のロジックを見直す、非同期処理やバッチ処理を検討することが必達です。

ただ、根本対策に時間を要してしまう可能性があったため、暫定対策としてのインフラ側のタイムアウト値設定を伸ばすことをやってみたのでまとめます。

4分(240秒)ぐらいかかる処理だったため、余裕を持って5分(300秒)ほど処理できるようにしたものです。

(WebAPIとしてありえないとは突っ込まないでほしい。一旦。。)

前提となる環境

  • GoogleComputeEngine(Linux CentOS)
  • GCPロードバランサ
  • nginx
  • Rails(APIモード)
    • Puma
  • Vue.js(axiosでAPI実行)

処理順序は?

基本的に処理が進むにつれてタイムアウト値は短くする必要があります。
①API URL実行
②GCPロードバランサ
③GoogleComputeEngine(Linux CentOS)
④nginx
⑤puma(Rails)

タイムアウト値を持っているのは?

今回の構成では以下2つのみがデフォルト保有しているものでした。
この2つを変更することでタイムアウト値を変えることが出来ます。
②ロードバランサ
④nginx

変更方法

②ロードバランサ

  1. 負荷分散メニューの下部の青文字「詳細設定メニュー」を押下
  2. 「バックエンドサービス」タブを選択
  3. 編集ボタンを押下
  4. バックエンドサービス詳細の上部の方にある鉛筆マーク image.png
  5. これで30秒→300秒に image.png

④nginx

nginxのタイムアウト初期値は60秒。290秒に変更する。
fastcgi_read_timeout proxy_read_timeout をセットする。

    server {
        listen 10443;
        server_name xxxx.hogehoge.com;

        # タイムアウトまでの秒数を変更
        fastcgi_read_timeout 290;
        proxy_read_timeout   290;
    }

まとめ

4分ほどかかる処理を作ってしまうのが、そもそもアンチパターンですね。
ただ、その処理をリファクタリングするのに時間をかけてユーザーを待たせる可能性があるのなら、長い時間を待たせて動かしてもらうのも対策の1つになり得ると思います。

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

【jQuery】activの時にFontAwesomeをつけたい

コトの顛末

アクティブ状態の箇所にカレントとしてFontAwesomeのアイコンをつけたかったのですが、
jQueryでaddClassすると思ったようなレイアウトにならない事案が発生。
hamlで直書きして調整するとあんなに楽なのになぁ...

ということで小技発見したのでメモがてら書きます。

問題の事案

FontAwesomeのclassをjQueryでaddClassすると、classはちゃんとつくのですが、
そのclassが付与されたところのレイアウトを合わせるのが大変だったので楽な方法ないかな〜と思ってました。

下記のように空の<i>タグを用意して、

index.hmlt.haml
%i.fa

それにclassつけたら擬似的に直書きと同じことができる!って思ったんですが...

ブラウザで表示を確認すると、空タグのせいかレンダリングされたコードの中にいないため、
jQueryのaddClassが不発になるという事案が発生しました。

このような処理で小技を実行しました。

まず、普通に FontAwesomeのアイコン使いたいところに書き込みます。
これでスタイルを調整しておきます。

index.hmlt.haml
%i.fa.fa-play

でも、アクティブじゃない時は classの「fa-play」が必要ないので削除します。
しかし!このままでは空タグ扱いとなり、ブラウザ上で表示されない(つまり
jQueryが効かない)ため、スペースのエンティティタグを入れます。

index.hmlt.haml
%i.fa  

「&nbsp」これを入れることにより、空欄判定にならず、jQueryが効きます。

test.js
$(".fa").addClass("fa-play");

こんな感じのjQueryをあてがってあげましょう。

b31635ca63e9307306ce485f2179b203.png

無事にactiv時だけカレントマークつきました!

終わりに

逆の方法として、すでについているクラスを消すは簡単な方法でしたが、
読み込みの際に、ズラ〜っと表示され、
読み込み完了後に、カレント箇所だけアイコンがついている状態になる。
読み込み完了後は良いですが、途中経過が気持ち悪かったので、
どうしてもちゃんと必要な箇所にだけアイコンをつける方式で実装したかったのです。
なので小技発見できてよかった!
これが本当に良い方法かはわかりませんが。

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

railsアプリではクラスインスタンス変数の注意する

クラスインスタンス変数の注意

右辺の実行結果をキャッシュするために以下の手法が使われる事があると思います。
1回目のメソッド実行時に実行結果をインスタンス変数@fooでキャッシュしておいて、2回目のメソッド実行時にはインスタンス変数の中身を返しています。

Class Foo
  def
    @foo ||= bar
  end
end

インスタンスメソッドの場合、インスタンス変数はFooクラスのオブジェクト毎に保持されます。
railsアプリ内のコードでこれをクラスメソッド内で使用する場合は注意が必要です。

Class Foo
  self.def
    @foo ||= bar
  end
end

クラスメソッド内でインスタンス変数はClassクラスのオブジェクト毎に保持され、
Classクラスのオブジェクトはrailsアプリケーション起動時から同じものを使い続けます。
その為、DB問合せ結果をキャッシュしてしまったりすると、アプリケーション起動後DBに初めて問い合わせした値を保持し続けてしまう為、その後別のリクエストでDBを更新しても、反映されない問題が発生します。

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

メモ

ストロングパラメータ

指定したキーのみを受け取るので、データをDBに入れるかどうかを判別させるために使う。
ex params.permit(:キー名,キーの値)

java script

list.push(***); 配列に要素を追加する。

list.pop(); 配列の最後の要素削除
⇅ 逆の関係
list.shift(); 配列の最初の要素削除

オブジェクトの作成
let object = {};

関数を使うとき
returnを記述する。
 returnは関数内で処理をした結果戻り値として返す。

関数とは
入力された 値に対して 出力して 返すこと

引数 → 関数 → 戻り値

function calc(num1,num2){
return num1*num2;
}

let num1 = 5;
let num2 = 7;
console.log(calc(num1,num2));

DOM (document object model)

htmlを階層上に表現したデータでjava script

body ---------header
|
|
------content
|
|
-------footer

body header content footer をノードと言う

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

Railsチュートリアル 第11章 リスト11.26のテストでテスト結果がRedになるはずのところでGreenになってしまった。

過去の2つの記事との脈絡が無いのが申し訳ないのですが、現在Railsチュートリアルの2周目を行っていて、あまりにもQiitaへの投稿をこまめにやっていなかったのが自分でも気になり、練習がてら自分がハマってしまったことについて投稿する練習をしてみようと思い投稿しました。
内容的にあまりにも初歩的な内容のため誰かの参考になるような記事では無いと思います。

問題

リスト11.26からリスト11.30までテストの結果がRailsチュートリアルが意図するものと違う結果になってしまっていた。
リスト11.26
https://railstutorial.jp/chapters/account_activation?version=5.1#code-generalized_authenticated_p

本来このリスト11.26ではRailsチュートリアルの説明通り引数の数が合わないことが原因となるArgumentErrorが出るはずであったが、何故かErrorは出ずにGreenになってしまった。

リスト 11.26: 抽象化されたauthenticated?メソッド red

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

リスト 11.26のキャプションに記したとおり、この時点ではテストスイートは redになります。

リスト 11.27: red

$ rails t

ここでRedになるはずだったのだが、
何故か自分の実際の環境ではGreenとなってしまった。

$ rails t
Running via Spring preloader in process 84886
Started with run options --seed 28770

  42/42: [=======================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.68394s
42 tests, 172 assertions, 0 failures, 0 errors, 0 skips

エラーであればエラーメッセージや結果に基づいてググるなりなんなりして解決出来ると考えたが、エラーではなく何も問題が無いと言われてしまうと今の自分の力では原因の特定は無理だと考えた。その後仕方なく進めて11.28などの作業をしてみた段階で新たなエラーが出たりしたらまたその時考えようと思い、11.28や11.29のリストの変更作業を行ってみた。その結果、リスト11.29ではGreenになるはずだったものが

Pry#input_array is deprecated. Use Pry#input_ring instead
19:34:42 - INFO - Running: test/models/user_test.rb
Running via Spring preloader in process 4899
Started with run options --seed 25892

ERROR["test_authenticated?_should_return_false_for_a_user_with_nil_digest", UserTest, 0.628540000000612]
 test_authenticated?_should_return_false_for_a_user_with_nil_digest#UserTest (0.63s)
ArgumentError:         ArgumentError: wrong number of arguments (given 2, expected 1)
            app/models/user.rb:39:in `authenticated?'
            test/models/user_test.rb:77:in `block in <class:UserTest>'

  12/12: [=========================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.71992s
12 tests, 19 assertions, 0 failures, 1 errors, 0 skips

となってしまった。
該当箇所となる

           app/models/user.rb:39:in `authenticated?'
            test/models/user_test.rb:77:in `block in <class:UserTest>'

の部分をいくらコードを見てみてもこの時は何がおかしいのか全く分からず途方に暮れてしまった。リストの内容でググってみても特にここでハマってしまったり自分と似たようなミスをしている人は居ないようで解決の手段が見当たらなかった。

原因

結局今の自力では原因を追求出来なかったのでRailsチュートリアルの11章終了時点でのリポジトリを見せてもらってそことの比較で自分のコードの悪い部分を見つけようと考えた。

またteratailに投稿があった、この内容も自分のコードのどこがおかしいかを考えるいいきっかけになった。
https://teratail.com/questions/214041
この質問とは結果的には自分の環境で起きていた問題とは違う原因や結果だったけど

①def current_userが2つ定義されていました。
⇛ 1つ削除しました。

の部分を見て自分でも同じように単純に同じ役割で重複しているコードがあるかもしれない?と考えることが出来るようになりました。

11章のuser.rbを確認すると
https://github.com/yasslab/sample_apps/blob/master/5_1_2/ch11/app/models/user.rb

お手本となるコードが以下の通りで

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save   :downcase_email
  before_create :create_activation_digest


  validates :name,  presence: true, length: { maximum:  50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true,
    length: { minimum: 6 }, allow_nil: true

  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  def User.new_token
    SecureRandom.urlsafe_base64
  end

  def remember
    self.remember_token = User.new_token
    self.update_attribute(:remember_digest,
      User.digest(remember_token))
  end

  def forget
    self.update_attribute(:remember_digest, nil)
  end

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = self.send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end

  def activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
  end

  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

  private

    def downcase_email
      self.email = self.email.downcase
    end

    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(self.activation_token)
      # @user.activation_digest => ハッシュ値
    end
end

その時の自分のリスト11.29までの終了時点でのuser.rbのコードと比較すると

user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save   :downcase_email
  before_create :create_activation_digest
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

    # トークンがダイジェストと一致したらtrueを返す
    def authenticated?(attribute, token)
      digest = send("#{attribute}_digest")
      return false if digest.nil?
      BCrypt::Password.new(digest).is_password?(token)
    end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end

  private

  # メールアドレスをすべて小文字にする
  def downcase_email
    self.email = email.downcase
  end

  # 有効化トークンとダイジェストを作成および代入する
  def create_activation_digest
    self.activation_token  = User.new_token
    self.activation_digest = User.digest(activation_token)
  end

  private
  # メールアドレスをすべて小文字にする
  def downcase_email
    # self.email = email.downcase
    email.downcase!
  end
end

お手本のコードと比較するとauthenticated?メソッドは1つしかなく自分のコードを見るとauthenticated?メソッドが2つあって重複していること自体がおかしいのではないか?ということに気付いた。

    # トークンがダイジェストと一致したらtrueを返す
    def authenticated?(attribute, token)
      digest = send("#{attribute}_digest")
      return false if digest.nil?
      BCrypt::Password.new(digest).is_password?(token)
    end




  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

対処法

重複しているコードを削除する。

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

テストの結果

このコードを削除することでGreenとなった。

Pry#input_array is deprecated. Use Pry#input_ring instead
19:57:47 - INFO - Running: test/models/user_test.rb
Running via Spring preloader in process 6751
Started with run options --seed 62215

  12/12: [=========================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.81365s
12 tests, 20 assertions, 0 failures, 0 errors, 0 skips

どうしてこういう初歩的なミスが起きたのか?

リスト 11.26: 抽象化されたauthenticated?メソッド red

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

この時点でこの変更点は書き換えでは無く追加だと思ってしまった。

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

このコードがあることに加えて
さらに

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

このコードを追記しなければならないと勘違いしてしまったが、
実際は追記するのではなくて

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

上記のコードを

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

と書き換えるのがリスト11.26での正しい作業だと気付けなかった。

単純に自分の知識や経験、常識の不足から来るミスだと思うし、同じ名前のメソッドが2つもあることを疑えなかった、おかしいと気付けなかったのが原因だと思う。

ちなみに1周目ではこのエラーを起こさずに普通にこなせていたので、おそらく1周目とは違う考え方で作業やって何事も無くこなせてしまっていたんだと思う。
1周目で起きたトラブルに2周目で対処するとかなら良いことだと思うけど、その逆で1周目の時は無かったエラーやトラブルを2周目でやらかすのはあまり良くないので反省。
1周目では何事も問題無かったのに2周目なら何故起きるんだろう?という手がかりも1周目やった時にはあまり残せていなかったので、その辺ののメモも今後たくさん取っておきたいと思う。

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

#Rails OR #Ruby + ActiveSupport の String#in_time_zone で 2月29日 30日 31日 が3月にずれるのだが

Rubyとか言語使用に関わる、深い事情ありの話かと思った。

Date.new を利用した方が良いかな。

require 'active_support/core_ext'

'2019-2-29'.in_time_zone('Tokyo').to_s
# => "2019-03-01 00:00:00 +0900"

'2019-2-30'.in_time_zone('Tokyo').to_s
# => "2019-03-02 00:00:00 +0900"

'2019-2-31'.in_time_zone('Tokyo').to_s
# => "2019-03-03 00:00:00 +0900"

'2019-2-32'.in_time_zone('Tokyo').to_s
# ArgumentError: argument out of range


Date.new(2019, 02, 28).in_time_zone('Tokyo')
# => Thu, 28 Feb 2019 00:00:00 JST +09:00

Date.new(2019, 02, 29)
# ArgumentError: invalid date

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2804

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

TrailBlazer概要まとめてみた

TrailBlazerを使う機会がありました。
日本語のリソースが少なかったので、軽くではありますがまとめておこうと思います。
まだ理解が若干ふわふわしているので、誤りなどあればご指摘いただけると幸いです!

Trailblazerとは?

Rubyのフレームワークに抽象レイヤーを提供するGemの集まりです。
(Railsのほか、HanamiやGrape, Sinatram等にも対応。また「TrailBlazer的設計スタイル」という意味では、どんな言語やフレームワークにも適用可能)

そもそものはじまりは、作者のNick Sutterer氏がRailsのMVC抽象レイヤーのあり方に疑問を持ったこと。Railsの手軽さを認める一方、ModelやControllerの肥大カオス化により、のちの保守性が下がることを問題視されたそうです(Nickさんの本意訳)。

Railsの設計思想の特徴ゆえに、迷子になりがちだったビジネスロジックの行き場をModelやController以外の場所に持ってくることができます。

Trailblazerを構成する主なGem

TrailBlazerは、Operation・Reform・Cells・Representableという主に4つのGemから構成されています。
Nickさん自身も述べていらっしゃいますが、すべてのGemを用いる必要はありません。
むしろ、「いつ」「どれくらい」「どの」要素を用いたいかに基づいて、部分的に各Gemを導入できるため、比較的容易に既存アプリケーションと併用することができます。

Operation

コントローラーに代わってビジネスロジックを担当するGemです。
stepによる簡潔な処理フローを強みとしています。

app/concepts/dinosaur/operation/create.rb
class Dinosaur::Create < TrailBlazer::Operation
  step Model( Dinosaur, :new )

  # あとで登場するFormオブジェクトをビルド
  step Contract::Build( constant: Dinosaur::Contract::Create )

  # ビルドしたFormオブジェクトをValidation
  step Contract::Validate()

  # ValidationしたFormオブジェクトをModelに反映
  step Contract::Persist()

end

TrailBlazerにおけるModelの役割は、RailsのModelの役割と基本的に同じです。
Controllerから指示を受けてデータのやり取りを行うか、Operationからの指示を受けるかというほどの違いで、永続化したいデータをDBに保存したり、その値を取得するポータルを提供することがメインのお仕事です。
以下のように、associationやscopeを記載します。

app/models/dinosaur.rb
class Dinosaur < ActiveRecord::Base
  belongs_to :ancient_creatures
  has_many :types

  scope :name, ->(name) { where(name: name) }
end

上記の例ではRailsのActiveRecordを使用していますが、Datamapperなど、ほかのORMでも問題ありません。
*ORM:Object Relational Mapping(オブジェクト関係マッピング)のこと。オブジェクト指向と関係データベースをつなぐことで、関係DBのレコードをオブジェクトとして認識し、直感的な操作を可能にしてくれる。

Reform

フォームオブジェクトを作るためのGemです。
ValidationやPersisting(savesync)をメインに、フォーム関連の処理をModelに代わって引き受けます。
contractは、Reform::Fromクラスを継承しており、必要項目のpropertyvalidates内容の定義を行う役割を持ちます。

app/concepts/dinosaur/contract/create.rb
module Dinosaur::Contract
  class Create < Reform::Form
    property :name
    validates :name, presence: true

    property :type do
      property :color 

      validates :color, presence: true
    end
  end
end

上記のバリデーションにはRailsのActiveModelを使用していますが、TrailBlazer v2.0以降であれば、dry-validationなども選択可能です。ただ、ActiveModelに関しては将来的に廃止を予定しているとのことです。

Cells

ビューモデル作成用のGemです。
表示関連の処理をModelに代わって引き受け、UIパーツを表現します。
別名「テンプレート・レンダリング」担当。

app/concepts/comment_cell.rb
class CommentCell < Cell::ViewModel
  def show
    render # app/concepts/comment/show.hamlをレンダリングする
  end
end

Representable

シリアライズ(デシリアライズ)用のGemです。
受け取ったオブジェクトをJSONやXML、YAMLに変換するほか、JSONからオブジェクトへの変換も可能です。
Operationの中に記述するほか、別ファイルに専用クラスを定義する方法があります。
例として、以下のようなオブジェクトをフォームから受け取るためには、

{
  "name" => "Velociraptor",
  "color"  => "Blue"
}

(方法1) Operationファイル内に記述する

app/concepts/dinosaur/operation/create.rb
class Dinosaur::Create < TrailBlazer::Operation
  ...
  representer do
    property :name
    property :color
  end
end

(方法2) 別ファイルにクラスを定義する(下記の例ではDinasourRepresenter

app/concepts/dinosaur/representer/dinasour_representer.rb
class DinasourRepresenter < Representable::Decorator
  include Representable::Hash # 受け取りたいオブジェクトのフォーマット
  include Representable::JSON # レスポンスとして返したいフォーマット

  property :name
  property :color
end

JSON形式でレスポンスを返すためには、

DinasourRepresenter.new(dinasour).to_json

おつかれさまでした!

お付き合いくださり、ありがとうございました!
今年も残り少し、よろしくおねがいします!

参考文献

TrailBlazer作者Nickさんの本
TrailBlazer Github
TrailBlazer Official
TrailBlazer Operation

以下のQiita記事を参考にさせていただきました!ありがとうございました!
@kazekyoさま
@kouheiszkさま
@yk-nakamuraさま

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

さくらVPSでCentOS7 11.RailsプロジェクトをGitで共同開発

はじめに

自由にテスト出来るLinuxのサーバーがほしくて、さくらVPSで構築してみました。
順次手順をアップしていく予定です。

前回インストールしたRuby On Railsを共同開発できるようにGitで管理したいと思います。

目次

  1. 申し込み
  2. CentOS7インストール
  3. SSH接続
  4. Apache・PHPインストール
  5. MariaDBインストール
  6. FTP接続
  7. sftp接続
  8. phpMyAdminインストール
  9. 環境のバックアップ
  10. Ruby On Railsインストール
  11. RailsプロジェクトをGitで共同開発

11.RailsプロジェクトをGitで共同開発

前回、Ruby On Railsインストール時に作成したテスト用プロジェクトHelloWorkdを共同開発できるようにGitで管理したいと思います。

共同開発用のマシンは、ローカルのWindows10のマシンにします。
構成は、こんな感じです。

共有リポジトリは、GitHub等を使う方法もありますが、今回はプロジェクトの有るさくらVPSサーバー内に作成します。

Gitでは、開発マシンのソースを直接本番ソースにアップするのではなく、一度共有リポジトリにアップしたものを本番ソースから取りに行く形になります。
本番ソースも開発と同様ローカルポジトリですので、開発側がアップした変更は手動で本番ソースに取り込む必要があります。
これだと不便ですので、共有リポジトリが変更を受け取った時自動的に本番ソースが取りに行くように設定します。

さくらVPSにGitインストール

インストール

$ sudo yum install git-all

インストール後の設定

ユーザ名とメールアドレスを設定します

$ git config --global user.name "sakura"
$ git config --global user.email sakura@kogueisya.com

gitの出力をカラーリング

$ git config --global color.ui auto

ディフォルトエディタの設定

$ git config --global core.editor vim

エイリアス設定(「checkout」を「co」に設定)

$ git config --global alias.co checkout

確認

$ git config user.name
sakura

$ git config -l
user.name=sakura
user.email=sakura@kougeisya.com
color.ui=auto
core.editor=vim
alias.co=checkout
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true

$ cd
$ cat .gitconfig
[user]
        name = sakura
        email = sakura@kougeisya.com
[color]
        ui = auto
[core]
        editor = vim
[alias]
        co = checkout

共有リポジトリ(ベアリポジトリ)作成

さくらVPSサーバーに共有リポジトリを作成します。

共有リポジトリ用ディレクトリ作成

/optの中に「プロジェクト名.git」というフォルダを作成します。

$ sudo mkdir -p /opt/helloworld.git/

-pは/optフォルダがなければ作成します。

所有者変更

フォルダの所有者を 「4.Apache・PHPインストール」で作成したWebコンテンツ操作用のグループ(webadmin)にします。

$sudo chown root:webadmin /opt/helloworld.git/
$sudo chmod 2775 /opt/helloworld.git/ -R

ベアリポジトリを作成

$ cd /opt/helloworld.git
$ git init --bare --share
Initialized empty shared Git repository in /opt/helloworld.git/

---bareは、ベアリポジトリを作成するオプション
--shareは、ベアリポジトリを複数のユーザによって共有可能にするオプション

本番ソース

前回「10.Ruby On Railsインストール」で作成したHelloWorldプロジェクトにローカルリポジトリを作成し、リモートリポジトリにプッシュします。

リポジトリのセットアップ

$ cd /var/www/app/HelloWorld
$ git init
Reinitialized existing Git repository in /var/www/app/HelloWorld/.git/

除外ファイル指定

railsコマンド実行時に作成される.gitignoreファイルにリポジトリから除外するファイルを指定するためのルールが記載されています。
このファイルに以下を追加します。

$ vi .gitignore
# Ignore other unneeded files.
doc/
*.swp
*~
.project
.DS_Store
.idea
.secret

ファイルをインデックスに追加(addコマンド)

プロジェクトのファイルをコミット待ちの変更が格納される「ステージングエリア」という一種の待機場所に追加

$ git add .

確認
ステージングエリアにあるファイルのリスト表示

$ git status

コミット

ローカルリポジトリへの変更反映

$ git commit -m "Initalize repository"

コミットメッセージの履歴参照

$ git log
commit d3f7327c74c7175b75ca7d78f4a1cd576f5b6d9a
Author: sakura <sakura@kougeisya.com>
Date:   Mon Dec 2 15:49:37 2019 +0900

    Initialize repository

リモートリポジトリにプッシュ

リモートリポジトリのアドレスに「origin」という名前を付けて記録
(「origin」にしたのは、pushやpullコマンドは実行時にリモートリポジトリ名を省略するとoriginという名前を使用する為)

$ git remote add origin /opt/helloworld.git

リポジトリをプッシュ

$ git push -u origin master
Counting objects: 6674, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (6272/6272), done.
Writing objects: 100% (6674/6674), 29.86 MiB | 8.65 MiB/s, done.
Total 6674 (delta 909), reused 0 (delta 0)
To /opt/helloworld.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

-uオプションは、「git push -u origin master」とすると次回から「git push」だけでpushしてくれます。

本番ソース反映の自動化

本番ソースも開発と同様ローカルポジトリですので、開発側がアップした変更は手動で本番ソースに取り込む必要があります。
これだと不便ですので、共有リポジトリが変更を受け取った時自動的に本番ソースが取りに行くように自動化します。

自動化にはフックを使います。
フックは、Gitでコマンドを実行する直前もしくは実行後に特定のスクリプトを実行する為の仕組みです。

フックの種類(サーバーサイド)

フック 概要
pre-receive クライアントからpushを受け取った直後に起動。
更新内容の確認が可能。
post-receive push内容の適用終了後起動。
通知を行う場合に便利。
update pushによる更新対称ブランチごとに起動。

共有リポジトリの中にある「hooks」の中に、フック名のスクリプトファイルを作成することで実現できます。

今回は、開発のローカルリポジトリからプッシュされ、その内容が適用された後に本番のローカルリポジトリからプルするスクリプトを実行したいので、「/opt/helloworld.git/hokks/post-receive」というスクリプトファイルを作成します。
スクリプトの内容は、本番ソースのドキュメントルートに移動し、publlを実行します。

$ vi /opt/helloworld.git/hooks/post-receive
#!/bin/sh
cd /var/www/app/HelloWorld/
git --git-dir=.git pull

スクリプトファイルを他のユーザから実行できるようにパーミッションを設定します。

$ chmod 755 /opt/helloworld.git/hooks/post-receive

以上でサーバー側の設定は完了です。

開発用ローカルマシン(Windows10)の設定

各ツールのインストール

Ruby、Rails、Git等をインストールしていきます。

Rubyインストール

下記サイトからダウンロードします。
最新版の「Ruby+Devkit 2.6.5-1 (x64)」をダウンロードしました。
https://rubyinstaller.org/downloads/

ダウンロードしたファイルを実行すると下記の画面が表示されます。
「◎I accept the License」を選択し[Next>]をクリックします。

下記画面が表示されたら、チェックボックス3つともチェックをいれ、[Install]をクリックします。

下記画面が表示されたらそのまま[Next>]をクリックします。

下記画面のようにインストールが開始されますので、終わるまでしばらく待ちます。

インストールが終了すると、下記画面が表示されます。
そのまま[Finish]をクリックすると画面が閉じ、インストールが完了します。

引き続き下記の画面が開きます。
1,2,3と入力して[Enter]キーを押すとインストールが開始されます。

インストールが完了すると下記画面のように「Which components shall be installed? If unsure press ENTER []」と表示されますので、[ENTER]キーを押すと画面が閉じます。

Railsに必要なgemをインストール

SQLite3インストール

コマンドプロンプトを開き、以下を実行します。

> ridk exec pacman -S mingw-w64-x86_64-sqlite3

下記画面のように「インストールを行いますか? [Y/n]」と聞いてきますので「Y」を入力します。

プロンプトに戻ったらインストール終了です。
引き続き以下のコマンドを実行します。

> gem install sqlite3 --platform ruby

下記画面のようにプロンプトに戻ったら完了です。

nokogiriインストール

コマンドプロンプトで以下を実行します。

> ridk exec pacman -S mingw-w64-x86_64-libxslt

下記画面のように「インストールを行いますか? [Y/n]」と聞いてきますので「Y」を入力します。

プロンプトに戻ったらインストール終了です。
引き続き以下のコマンドを実行します。

> gem install nokogiri --platform ruby -- --use-system-libraries

下記画面のようにプロンプトに戻ったら完了です。

Node.jsインストール

下記サイトからダウンロードします。
https://rubyinstaller.org/downloads/
「LTS推奨版」「Windows Installer (.msi)」の「64-bit」をダウンロードしました。

ダウンロードしたファイルを実行すると下記の画面が表示されますので[Next]をクリックします。

下記画面が表示されたら「□I accept the terms in the License Agreement」にチェックを入れ[Next]をクリックします。

下記画面が表示されますので、そのまま[Next]をクリック。

下記画面もそのまま[Next]クリック。

下記画面が表示されたら「Automatically install the ・・・」にチェックをいれて[Next]をクリック。

下記画面の[Install]クリックで、インストールが開始されます。

インストールが終わるまでしばらく待ちます。

インストールが終わると下記画面が開きますので[Finish]をクリックすると画面が閉じます。

引き続き下記の画面が開きます。
「継続するには何かキーを押してください ...」と2回表示されますので都度[Enter]キーを押してください。

下記の画面が開き、インストールが開始されます。

下記のように「Tyoe ENTER to ext:」と表示されたら完了です。
[Enter]キーを押すと画面が閉じます。

Bundlerインストール

コマンプロンプトを開き下記のコマンドを実行します。

> gem install bundler

Gitインストール

GUIでGitが使えるTortoiseGitを使います。
画面のキャプチャは取っていませんでしたm(__)m

Git For Windows

下記サイトの[Download]ボタンよりダウンロード
ダウンロードしたファイルを実行し、画面に従いインストール
https://gitforwindows.org/

TortoiseGit

下記サイトのfro 64-bit Windowsの「Download TortoiseGit 2.9.0-64.bit(~19.5Mib)」をクリックしてダウンロード
ダウンロードしたファイルを実行し、画面に従いインストール
https://tortoisegit.org/download/

メニュー等を日本語に設定

下記サイトからLanguage PacksのJapanese 64Bitの[Setup]をクリックしてダウンロード
ダウンロードしたファイルを実行し、画面に従いインストール
https://tortoisegit.org/download/

インストール後、デスクトップの適当な場所で右クリックし表示されるメニューから[TortoisGit]-[設定]を選択

開いた画面の[全般][TotoiseGit][言語(Language)]より「日本語(日本)」を選択、[OK]ボタンをクリック

以上でWIndows10の設定は完了です。

クローンを作成

共有リポジトリからクローンを作成します。
作成場所は、C:\Prj\HelloWorldにします。

TortoiseGitを利用してクローン作製

デスクトップ等適当な場所で右クリックし、表示されたメニューから[Gitクローン (複製)...]を選択します。

下記の設定画面が開きます

URLには以下の値を設定します。
"ssh://" + ユーザー名 + "@" + さくらVPSサーバーのアドレス + ":" + SSHポート番号 + "共有リポジトリディレクトリ"

ディレクトリは、クローンを作成するフォルダ名を設定します。
ここでは、「C:\Prj\HelloWorld」にしました。
フォルダは無ければ作成されます。

Putty鍵のロードにチェックを入れ、[...]をクリックし、秘密鍵を選択します。
ここで選択する秘密鍵は、「 7.sftp接続」の設定時に作成した秘密鍵ファイルです。

[OK]をクリックすると、秘密鍵のパスフレーズを聞いてきますので、「 7.sftp接続」で設定したパスフレーズを入力し[OK]をクリックします。

クローン作製中

無事終了しました

フォルダ確認

動作確認

サーバーを起動してみる

コマンドプロンプトを起動し、ディレクトリの移動。

> cd \Prj\HelloWorld

とりあえずサーバーを立ち上げてみたら、railsコマンドがみつからないので、「bundle install」しろと出ました。

>bundle exec rails s
bundler: command not found: rails
Install missing gem executables with `bundle install`

「bundle install」を実行すると、派手にエラーが出ました(T0T)

>bundle install
Fetching gem metadata from https://rubygems.org/.
Retrying fetcher due to error (2/4): Bundler::PermissionError There was an error while trying to read from `C:/Users/sugi/.bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/mini_portile2`. It is likely that you need to grant read permissions for that path.
.
Retrying fetcher due to error (3/4): Bundler::PermissionError There was an error while trying to read from `C:/Users/sugi/.bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/thread_safe`. It is likely that you need to grant read permissions for that path.
.
Retrying fetcher due to error (4/4): Bundler::PermissionError There was an error while trying to read from `C:/Users/sugi/.bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/websocket-driver`. It is likely that you need to grant read permissions for that path.
.
There was an error while trying to read from
`C:/Users/sugi/.bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/globalid`.
It is likely that you need to grant read permissions for that path.

どうもファイルの読み取り権限に問題があるようです。
調べてもよくわからなかったので、ちと乱暴ですが管理者としてコマンドプロンプトを実行してみました。

再度「bundle install」したら、無事動きました。

サーバー起動に再チャレンジ。

>bundle exec rails server -b 0.0.0.0
=> Booting Puma
=> Rails 5.2.4 application starting in development
=> Run `rails server -h` for more startup options
  Please add the following to your Gemfile to avoid polling for changes:
    gem 'wdm', '>= 0.1.0' if Gem.win_platform?
  Please add the following to your Gemfile to avoid polling for changes:
    gem 'wdm', '>= 0.1.0' if Gem.win_platform?
*** SIGUSR2 not implemented, signal based restart unavailable!
*** SIGUSR1 not implemented, signal based restart unavailable!
*** SIGHUP not implemented, signal based logs reopening unavailable!
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.5-p114), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

ブラウザで確認。
動いているようです。

コントローラーを作成しプッシュしてみる

コントローラを作成

>bundle exec rails generate controller hello
  Please add the following to your Gemfile to avoid polling for changes:
    gem 'wdm', '>= 0.1.0' if Gem.win_platform?
  Please add the following to your Gemfile to avoid polling for changes:
    gem 'wdm', '>= 0.1.0' if Gem.win_platform?
      create  app/controllers/hello_controller.rb
      invoke  erb
      create    app/views/hello
      invoke  test_unit
      create    test/controllers/hello_controller_test.rb
      invoke  helper
      create    app/helpers/hello_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/hello.coffee
      invoke    scss
      create      app/assets/stylesheets/hello.scss

app/controllers/hello_controller.rbにアクションメソッドindexを追加

class HelloController < ApplicationController
  def index
    render plain: 'こんにちは、世界!'
  end
end

config/routes.rbにルーティング設定

Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  get 'hello/index', to: 'hello#index'
end

ローカルサーバーを起動し動作確認

> bundle exec rails s

プッシュ

変更したファイルをさくらVPSサーバの共有リポジトリにプッシュします。
その前に、ローカルリポジトリに変更をコミットします。

HelloWorldフォルダの上で右クリックし、表示されたメニューより[Gitコミット(C)->"master"...]を選択。

下記のようにコミット画面が表示されますので、「メッセージ」を入力し、追加ファイルにチェックを入れ[コミット]ボタンをクリックします。

下記画面のようにコミットが実行され、成功と表示されるとコミットは成功です。

コミットが成功したら、共有リポジトリへプッシュします。
上記画面の[プッシュ]ボタンをクリックすると下記のプッシュ画面が開きます。
[OK]をクリックし、サーバーの共有リポジトリにプッシュします。

プッシュが実行されます。
無事成功しました。

さくらVPSサーバーでの動作確認

Git確認

共有リポジトリへのプッシュ確認

$ cd /opt/helloworld.git
$ git log
commit 941e28b8e44d6dfcbe2ba2bdf5189cba76b92b38
Author: Kouichi Sugimoto <sugi@kougeisya.com>
Date:   Tue Dec 3 18:06:20 2019 +0900

    Windowsからのプッシュテスト

commit d3f7327c74c7175b75ca7d78f4a1cd576f5b6d9a
Author: sugi <sugi@kougeisya.com>
Date:   Mon Dec 2 15:49:37 2019 +0900

    Initialize repository

プッシュはうまくいっているようです。
開発リポジトリから共有リポジトリにプッシュがあると、自動的に本番サーバーがプルするように設定していました。
これが動いているかログをチェックします。

$ cd /var/www/app/HelloWorld
$ git log
commit 941e28b8e44d6dfcbe2ba2bdf5189cba76b92b38
Author: Kouichi Sugimoto <sugi@kougeisya.com>
Date:   Tue Dec 3 18:06:20 2019 +0900

    Windowsからのプッシュテスト

commit d3f7327c74c7175b75ca7d78f4a1cd576f5b6d9a
Author: sugi <sugi@kougeisya.com>
Date:   Mon Dec 2 15:49:37 2019 +0900

    Initialize repository

hello_controller.rbが出来ているかチェック

$ cd /var/www/app/HelloWorld/app/controllers
$ ls
application_controller.rb  concerns  hello_controller.rb

動作確認

本番ソースのサーバーを起動します。

$cd /var/www/app/HelloWorld
$ rails server -b 0.0.0.0
=> Booting Puma
=> Rails 5.2.4 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.5-p114), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

ブラウザで確認

無事動きました。
これでGitを使った共同開発の環境が出来ました。

次回

次回は、Pythonのインストールの予定です。

前回:Ruby On Railsインストール

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

【Rails】has_manyをhas_oneに変えた時の挙動

has_manyで複数のレコードが紐づいている状態で、has_oneに変えるとどのレコードが紐づくのかふと気になったので試してみた。
言い換えると、has_many関係でarticle.commentsが複数取れる状況だった時に、has_oneに変更してarticle.commentを実行すると、どのレコードを取ってくるのかを見てみた。

結論

created_at, updated_atの値に関係なく、idの一番若いレコードと紐づく

一番新しいレコードを取ってくるのかなぁと思ったらそうではなかった。
上の結論に至るまでの検証を下にまとめていく。

前提

ArticleモデルとCommentモデルを用意し、Article has_many Commentsの関係をとるとする。

article.rb
class Article < ApplicationRecord
  has_many :comments
  # has_one :comment #検証用
end
comment.rb
class Comment < ApplicationRecord
  belongs_to :article
end

用意したレコードはarticle1個comment3個。commentは全てarticleに紐づいてるものとする。

article
#<Article:0x00007f979bad9f00
 id: 1,
 title: "title1",
 created_at: Tue, 03 Dec 2019 04:54:13 UTC +00:00,
 updated_at: Tue, 03 Dec 2019 04:54:13 UTC +00:00>
comment
[#<Comment:0x00007f9798e79f28
  id: 1,
  content: "comment1",
  article_id: 1,
  created_at: Tue, 03 Dec 2019 04:55:15 UTC +00:00,
  updated_at: Tue, 03 Dec 2019 04:55:15 UTC +00:00>,
 #<Comment:0x00007f9798e79d98
  id: 2,
  content: "comment2",
  article_id: 1,
  created_at: Tue, 03 Dec 2019 04:55:23 UTC +00:00,
  updated_at: Tue, 03 Dec 2019 04:55:23 UTC +00:00>,
 #<Comment:0x00007f9798e79c08
  id: 3,
  content: "comment3",
  article_id: 1,
  created_at: Tue, 03 Dec 2019 04:55:26 UTC +00:00,
  updated_at: Tue, 03 Dec 2019 04:55:26 UTC +00:00>]

検証1 上記条件でhas_many => has_oneに変更

上記の状態で article.rb の has_many をコメントアウトし、 has_oneに変更
Article.first.comment実行

[1] pry(main)> Article.first.comment

結果

id = 1のcomment

result
=> #<Comment:0x00007f979b488638
 id: 1,
 content: "comment1",
 article_id: 1,
 created_at: Tue, 03 Dec 2019 04:55:15 UTC +00:00,
 updated_at: Tue, 03 Dec 2019 04:55:15 UTC +00:00>

=> 一番若いidを取ってくる

結論の通り。だけどこれだけではupdated_atとcreated_atの影響の可能性も捨てきれないので、もうちょい掘り下げる。

検証2 update_atの値を変える

id=1のcommentをアップデートし、updated_atを他二つより最近のものにする
has_oneに変更し、Article.first.comment実行

[1] pry(main)> Comment.first.update_attributes(content: "comment1A")
[2] pry(main)> Article.first.comment

結果

id = 1のcomment

result
=> #<Comment:0x00007f979b675568
 id: 1,
 content: "comment1A",
 article_id: 1,
 created_at: Tue, 03 Dec 2019 04:55:15 UTC +00:00,
 updated_at: Tue, 03 Dec 2019 04:59:09 UTC +00:00>

=> updated_atの値に関係なく一番若いidを取ってくる
updated_atの値を見てわかる通り。

検証3 id=1 のCommentレコードを削除

id=1のcommentを削除
has_oneに変更し、同様に実行

[1] pry(main)> Comment.first.delete
[2] pry(main)> Article.first.comment

結果

id = 2のcomment

result
=> #<Comment:0x00007f979bbe3540
 id: 2,
 content: "comment2",
 article_id: 1,
 created_at: Tue, 03 Dec 2019 04:55:23 UTC +00:00,
 updated_at: Tue, 03 Dec 2019 04:55:23 UTC +00:00>

=> 存在しているレコードの中で一番若いidを取ってくる
commentのidが2と3しかない状態なのでこのようになる

検証4 再び id=1 のCommentレコードを追加

検証3を終えた状態 (id=1のcommentを消した状態) で
id=1のcommentを再びcreate。articleに紐づける
has_oneに変更し、同様に実行

[1] pry(main)> Comment.create(id:1, content:"comment1", article_id:1)
[2] pry(main)> Article.first.comment

結果

id = 1のcomment

result
=> #<Comment:0x00007f979b88a678
 id: 1,
 content: "comment1",
 article_id: 1,
 created_at: Tue, 03 Dec 2019 05:05:59 UTC +00:00,
 updated_at: Tue, 03 Dec 2019 05:05:59 UTC +00:00>

=> created_atの値に関係なく一番若いidを取ってくる
今までは"idが一番若い = created_atの値が一番古い"が成り立ってたたため、これでcreated_atの値が関係ないことがわかった。
一度消したのでid=2をとってくるかと思いきやid=1だった。

終わりに

結論の通り、has_manyからhas_oneに変更した場合、idの一番若いものを取ってくることがわかった。直感的には一番新しいレコードを取ってきそうな気はするけど、そうではないっぽいので気をつけたい。検証不足、間違い等ありましたらコメントお願いしますm(_ _)m

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

ハッカソンの開催情報を自動でお知らせするBotをGithub Actionsに移行して運用費が0円になりました

新着のハッカソン・ゲームジャム・アイディアソン・開発合宿の情報を自動的にお知らせしているBotがあります。
よかったらフォローしてください!!

このBotの頭の中身についてはこちらの記事にて紹介しました。

最新のハッカソンの開催情報を自動で集めて、お知らせするBotを作ったので頭の中を紹介

また、具体的なソースコードも公開していますのでこちらを参照してください。

hackathon_portal

今回はこのHackathonPortalをこれまで、AWS Lightsail (Ubuntu)サーバーにて稼働していたものを Github Actions に完全に移行したので、その内容について紹介します。

Github Actionsの本来の使い方について

本来の Github Actions の正しい使い方はCI/CDがメインで主に以下のような開発支援として用いられます。

  • 自動的にデプロイ
  • 自動テストの実行
  • issueやプルリクのお掃除

など

今回は Github Actionsschedule機能 にのみ着目して活用しました。
また Github Actions の本来の使い方については こちら などにまとめています。

Github Actionsにて自動的にデプロイする環境作成(Webサイト編)

そもそもなぜ、Github Actionsに移行したのか?

  • Github Actions で稼働させる間は月額費用が0円になるため
    • Github Actions の費用はPublicリポジトリならいくら使っても0円です。(privateは無料枠を超えたら費用がかかります。詳しくはこちら)
    • 元々,HackathonPortalはPublicリポジトリで稼働しています。
  • 下記の仕様の関係上、 schedule機能 さえあれば、要件を満たせるので Github Actions の使用に適していたため

仕様

HackathonPortalの仕様を簡単にまとめると以下になります

  • 1日に1回イベント公開されたイベントの情報を集める → cron を活用
  • 集めた情報をデータベースに保存する
  • 保存したデータベースの情報を整理して、Twitterなどに発信する

基本的に定期的に Batch が実行されればいいので Github Actionsschedule機能 を使えば実現できます。

実現させるにあたって苦労したこと

データベース

Github Actions ではMySQLなどのミドルウェアを使用することはできますが、 Github Actions では毎回実行されるたびにデータが空っぽになります。
そのため Github Actions のみで完結させようとした場合、これまでためたデータをどこかに保持するためておく仕組みが必要です。
外部のデータベースを活用するなど考えましたがいずれの場合も

  • 結局、月額費用がかかる
  • 無料のデータベースは容量が少ない
  • 何かと制約が多い

ということで結局これまでためたデータを全てGitの管理下に置いてGithubに保管してもらうことで対応しました。

具体的には以下のフローを Github Actions の中で行なっています

  1. MySQL稼働
  2. MySQLの中にデータを挿入する
  3. データを集める → 発信する
  4. mysqldump してデータを全て SQLファイル で出力する
  5. git push で Githubにアップロード

こうすることで Github Actions (Github) のみで完結する仕組みを構築しました。

実際に設定している内容については こちら を参照

データ容量におけるGithub上の仕様

Github上では 100MB を超えるデータをpushすることができません。
(詳しくは こちら)

1ファイル 100MB を超えるデータを保存する場合は Githubでは Git LFS を使用する必要があります。
しかし、 Git LFS を使用してのデータ管理は 月間1GB を超えると月額費用が発生してしまします。

今回、月額費用0円での運用を実現したかったので、mysqldump で抽出するSQLを 1ファイル100MB未満 になるように分割してpushするようにしました。そのために以下のようなコマンドを Batch の中で実行しています。

mysqldump データベース名 テーブル名 -u ユーザー名 -pパスワード --no-create-info -c --order-by-primary --skip-extended-insert --skip-add-locks --skip-comments --compact > SQLファイル.sql

上記のように mysqldump をするときに

  • --no-create-info でデータのみを取得し
  • --skip-extended-insert で一行ずつINSERT文を抽出
  • --skip-add-locks --skip-comments --compact でコメントなどの不要なものを全て排除

したSQLファイルを出力します。そして以下のように出力したSQLファイルを split コマンドを使い分割しています。

split -l 10000 -d --additional-suffix=.sql fulldump.sql テーブル名/

上記の場合10000行ごとに分割しています。これにより1ファイル 大体 100MB 以下になるように分割しています。

split コマンドの --additional-suffix オプションはLinuxでは使用可能ですが、Mac OS では使用することができません。Mac OS--additional-suffix オプションを使用する場合は gsplitbrew install gsplit にてインストールして gsplit コマンドにすることで使用することができます。

実行されるブランチをmaster以外に切り替える

基本的に Github Actionsschedule機能default ブランチ(最初は master ブランチ)の内容が実行されます。
今回データを蓄積するブランチは master ブランチ以外で行いたかったです。そのため、Github Actions が実行された瞬間にブランチを切り替えるには Github Actionsが実行される yml にて使用されている、actions/checkout (uses: actions/checkout@v1 の部分) で、with:とともにブランチを一緒に指定することで実行できました。(ref: ブランチ名)

以下にその部分を記述します。

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
      with:
        ref: crawled-data

Github Actions内からGithubへpushする

Github Actions の中からGithubへのpushをそのまま行おうとするとエラーとなってしまいます。
今回、pushを可能にするために Personal access tokens を発行し、User Name(Githubのアカウント名) とともに以下のように設定します。

git remote set-url origin "https://${User_Name}:${Personal_Access_Tokens}@github.com/TakuKobayashi/hackathon_portal.git"

この状態で

git push origin ブランチ名

を行うことで、Github Actions の中からGithubへのpushを実現しています。

Personal access tokens の発行の仕方はこちらを参照してください。

コマンドライン用の個人アクセストークンを作成する

課題

Github Actionsで稼働しているマシンスペックの問題

Github Actionsで稼働しているマシンのストレージ容量は 14GB です。

【参考】

mysqldump したSQLファイルの容量が 14GB を超えるようなデータ量となるとき、別の方法を考える必要があります。
(現状 1GB も超えていないのでまだ大丈夫)

最後に

ハッカソン・ゲームジャム・アイディアソン・開発合宿への参加に興味がある皆さんは是非フォローしてください!!
よろしくお願いします。

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

Rails 6.0.x標準で Ajax+(jQuery+Partial) でHTML部分更新する世界一シンプルなサンプル

TL;DR (長い! 3行で!)

↓ の記事をRails 6.0.1 版に書き直したものです.
Rails 5.x標準で Ajax+(jQuery+Partial) でHTML部分更新する世界一シンプルなサンプル

Front EndがWebpackに変わっており,
・ Coffee Script → JavaScript への変更
・ jQuery導入方法の変更
が大きな変更点です.

動作確認環境

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.6 LTS ← 古いLTSなのでわりとダメ,できれば最新LTS使ってください
Release:        14.04
Codename:       trusty

$ ruby --version
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]

$ rails --version
Rails 6.0.1

$ yarn --version
1.16.0

$ nvm --version
0.35.1

$ node --version
v12.13.1

NOTICE

nodeのversionが古いと(LTSじゃないと?),$ rails newの途中で ↓ のようなErrorが出ます.

error get-caller-file@2.0.5: The engine "node" is incompatible with this module. 
Expected version "6.* || 8.* || >= 10.*". Got "9.10.1"
error Found incompatible module.

これ以降,webpackが使えなくなります.
(error Command "webpack" not found.)
$ rails new自体は "Webpacker successfully installed" のメッセージがでて正常終了してるように見えてしまうため気付けない...

ざっくりシーケンス

だいたい ↓ のようなシーケンスを実現します.
Client-Server間でSession張って双方向通信とかそういうところはRails Frameworkが全部やってくれます.

Front End : UserがInputしたTextをAjaxでBack End側にPost
↓
Back End : PostされたTextとPartialを使って部分的に差し替えるHTMLをRendering
↓
Front End : JavaScriptでAjaxのCallback(部分的に差し替えるHTML)を受け取る
↓
Front End : jQueryでHTMLの部分差し替え


Rails 6 でFront EndがWebpack化されたことで,Rails 5 に比べてReactなども導入しやすくなっています.そのため,Back Endから返すCallbackはpre-renderしたHTMLでなく,Jsonなどでデータを返して,Front End側でrenderする,という方法もあります.

Project初期化 + Default動作確認

$ rails new ajax-test

でRails Projectを初期化します.$ cd ajax-testでRailsのrootに移動して,

$ rails server

でサーバを起動して,
http://localhost:3000/
にアクセスして "Yay! You're on Rails!" のDefaultページが出たらOKです.

Static Page作成

Ajax動作のベースになるStatic Page部分を実装します.

Controller

$ rails generate controller AjaxTest

でControllerのTemplateを作成後,Action(Method)を追加します.

app/controller/ajax_test_controller.rb
class AjaxTestController < ApplicationController
  def top
    # NOP.
  end

  def update
    # TBD.
  end
end

top : Topページ表示用,特に処理はありません.
update : Ajaxリクエストを受ける用,実装はあとで追加します.

Route Config

ControllerのActionに繋げるためのRoute定義を追加します.

config/routes.rb
get  'ajax_test/top',    to: 'ajax_test#top',    as: 'ajax_test_top'
post 'ajax_test/update', to: 'ajax_test#update', as: 'ajax_test_update'

GET : Topページ表示用.
POST : Ajaxリクエスト用.

View

ajax/test/top にアクセスしたときに表示するViewを追加します.

app/views/ajax_test/top.html.erb
<div id="request_ajax_update" >
  <%= form_tag(ajax_test_update_path, method: :post, remote: true) { %>
    <%= text_field :data, :text %>
    <%= submit_tag 'Post AJAX' %>
  <% } %>
</div>

<hr>

<div id="updated_by_ajax" >
  DEFAULT
</div>

<hr>

大きく2つのブロックだけです.

<div id="request_ajax_update" >
Ajax RequestをPostするFormを持つブロック.
Userが入力したTextを params[:data][:text] に詰めて,
ajax_test_update_path ( = /ajax_test/update) にPOSTします.

<div id="updated_by_ajax" >
Ajax Callbackを受けてHTMLの部分更新をするブロック.

Static Page動作確認

この時点で,
http://localhost:3000/ajax_test/top
にアクセスすると,↓ のようなページが表示されると思います.
default.png

ただし,Ajaxの実装がまだ無いので,Post AJAX ボタンをClickしても見た目上は何も起こりませんが,$ rails server が動いているConsoleにText Fieldに入れた文字列がParametersとして通知されているのを確認できると思います.

Parameters: {"data"=>{"text"=>"Hello World !"}, "commit"=>"Post AJAX"}

Ajax実装

いままでに作ったStatic PageにAjax実装を組み込んでInteractiveな機能を追加します.

Partial View

部分的に更新するHTMLの部品を追加します.

app/views/ajax_test/_ajax_partial.html.erb
<div>
  Callback Msg = <%= results[:message] %>
</div>

このPartialで <div id="updated_by_ajax" > の中身を差し替えます.

Controller Action

さきほど作ったControllerの update の中身を実装します.

app/controller/ajax_test_controller.rb
class AjaxTestController < ApplicationController
  def top
    # NOP.
  end

  def update
    post_text = params[:data][:text]
    results = { :message => post_text }
    render partial: 'ajax_partial', locals: { :results => results }
  end
end

Controllerに渡ってきた Parameters の中に格納されているUserが入力したTextを使ってPartialをrenderしています.
Partialのファイル名 _ajax_partial.html.erbajax_partial で使えるあたりはRailsの規約に沿っています.

ここで,

res = render_to_string partial: 'ajax_partial', locals: { :results => results }
puts res

のようなCodeを書いておくとPartialをrenderしたときの実際のOutputが見れます.

<div>
  Callback Msg = Hello World !
</div>

JavaScript実装

Rails 6 からFront EndにWebpackが標準で使われることになり,Coffee ScriptがDefaultで使われなくなっています.

かわりに,
app/javascript/packs/application.js
のようなWebpackに対応したJavaScriptのDir構成に変わっています.
(Rails 5 でWebpack使っていた人にはおなじみの構成かも)

ViewにJavaScriptのEntry Pointを追加

ViewのHTMLにJavaScriptの読み込み部分を追加します.

app/views/ajax_test/top.html.erb
<div id="request_ajax_update" >
  <%= form_tag(ajax_test_update_path, method: :post, remote: true) { %>
    <%= text_field :data, :text %>
    <%= submit_tag 'Post AJAX' %>
  <% } %>
</div>

<hr>

<div id="updated_by_ajax" >
  DEFAULT
</div>

<hr>

<!-- ↓ 追加 -->
<%= javascript_pack_tag 'ajax_test' %>
<script>
  register_callback();
</script>

<%= javascript_pack_tag 'ajax_test' %>
app/javascript/packs/ 以下に置いてある ajax_test というファイル(の中身)を読み込む,という指示です.
実際にはWebpackがひとかたまりの.jsにしてしまうので,この名前のファイルを読み込んでいるわけではありません.

その後,<script></script> タグで register_callback() という関数を呼び出します.
この関数の実装は次で追加します.

JavaScript本体の実装

Webpack用Dir構成に沿って,Viewから呼び出せるように ↓ のJavaScriptを追加します.

app/javascript/packs/ajax_test.js
import * as $ from "jquery";

function register_callback() {

  $("#request_ajax_update").on(
      "ajax:complete",
      function(event) {
        var res = event.detail[0].response
        $('#updated_by_ajax').html(res)
      }
  );

}

window.register_callback = register_callback;

Viewから呼び出すregister_callback()関数の中で,AjaxリクエストをPOSTする #request_ajax_update Tagに ajax:complete のCallback関数を登録しています.

Viewから呼び出せるようにするため,
window.register_callback = register_callbcak;
の行でwindowの名前空間(global)にexportしています.
注) この方法は動作はしますがあまり行儀よくないはずです...

ただし,このままでTopページからPost AJAXをClickしても何も起こりません.
ChromeのDeveloper Tool等でConsole Logをみると,

Uncaught Error: Cannot find module 'jquery'
    at webpackMissingModule (ajax_test.js:1)
    at Module../app/javascript/packs/ajax_test.js (ajax_test.js:1)

のようなError Logがでていて,jquery の名前解決ができてないことがわかります.

jQueryを使えるようにする

Rails 5 から引き続きRails標準ではjQueryはサポートされていないため,WebpackからjQueryを使えるようにします.

$ yarn add jquery

のCommandでjqueryをInstallします.

必要かもしれない追加

"rails6 + jquery" とかのキーワードでぐぐると,↓ の2つの追加も必要,と出てきますが,手元の環境だと必要ありませんでした.
RailsのVersionによるのか,他の環境によるのか...

ref: https://www.botreetechnologies.com/blog/introducing-jquery-in-rails-6-using-webpacker

app/javascript/packs/application.js
// 他のrequireがいろいろ...

require("jquery") // 追加

config/webpack/environment.js
const { environment } = require('@rails/webpacker')

// 追加
const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/src/jquery',
    jQuery: 'jquery/src/jquery'
  })
)

module.exports = environment

Ajax動作確認

これで必要な変更はすべてです.
http://localhost:3000/ajax_test/top
にアクセスして,Text Fieldになにか文字列を入力してPost AJAXボタンを押したら,DEFAULT の文字列が更新されて ↓ のような画面になっていれば成功です.

ajax.png

ChromeのDeveloper ToolなどでDOM構造を見てみると,↓ のようになっていて,<div id="updated_by_ajax" >の中身が差し替わっているのが見えます.

dev_tool.png

おわり

Rails 6 でAjaxを使ったシーケンスを一本通すまでをできる限りDefaultのままで実現してみました.
環境依存でたまたまうまく動いている部分などあるかもしれません.もし何かおかしな点がありましたら教えていただけると助かります.

---///

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

Railsチュートリアル 第11章 アカウントの有効化 - AccountActivationsリソース

前提

  • アカウントの有効化機能は、セッション機能と同様、RESTfulなリソースとしてモデル化する
    • AccountActivationsコントローラーの実装は、Sessionsコントローラーの実装と似た形になる
    • ただし、以下のユースケースから、ここまで実装してきたリソースとは異なる形の実装となる
  • 「有効化リンクはメールでユーザーに送信される。ユーザーが当該リンクをクリックすることにより、アプリケーションでアカウントの有効化プロセスが開始される」というユースケースである
    • Webブラウザでリンクをクリックした場合と同様、このときユーザーから送られるのはGETリクエストである
    • GETリクエストが直接のトリガーになるゆえ、「editアクションで直接RDBの内容が変更される」という実装になる

AccountActivationsコントローラー

AccountActivationsコントローラーの生成

まずはrails generate controllerコマンドにより、AccountActivationsコントローラーを生成するところから始まります。この部分は、Sessionsコントローラーと同じ手順ですね。

# rails generate controller AccountActivations
Running via Spring preloader in process 13245
      create  app/controllers/account_activations_controller.rb
      invoke  erb
      create    app/views/account_activations
      invoke  test_unit
      create    test/controllers/account_activations_controller_test.rb
      invoke  helper
      create    app/helpers/account_activations_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/account_activations.coffee
      invoke    scss
      create      app/assets/stylesheets/account_activations.scss

AccountActivationsリソースに関係するルーティング

AccountActivationsリソースのユースケースは、「ユーザーがリンクをクリックすることにより、アプリケーションでアカウントの有効化プロセスが開始される」というものです。当然ながら、「アカウントの有効化プロセスを開始するためのリンク」が必要になってきます。さらに、「RESTfulなリソースとしてモデル化する」という前提があります。というわけで、「アカウントの有効化プロセスを開始するためのリンク」は、「AccountActivationsリソースのeditアクションに紐付けされたリンク」ということになります。

edit_account_activation_url(activation_token, ...)

AccountActivationsリソースのユースケースは、「ユーザーがリンクをクリックすることにより、アプリケーションでアカウントの有効化プロセスが開始される」というものに限られます。(editアクションではなく)リソースそのものへのGETや、(Usersリソースとセットでない)単独でのPOSTDELETEUPDATE、以上のリクエストが送られてくることは想定されません。ゆえに、必要となるルーティングはeditのみに限られます。

resources :account_activations, only: [:edit]

結果、Railsのルーティング(config/routes.rb)は、以下のように変更する必要があります。

config/routes.rb
  Rails.application.routes.draw do
    root    'static_pages#home'
    get     '/help',    to: 'static_pages#help'
    get     '/about',   to: 'static_pages#about'
    get     '/contact', to: 'static_pages#contact'
    get     '/signup',  to: 'users#new'
    post    '/signup',  to: 'users#create'
    get     '/login',   to: 'sessions#new'
    post    '/login',   to: 'sessions#create'
    delete  '/logout',  to: 'sessions#destroy'
    resources :users
+   resources :account_activations, only: [:edit]
  end

演習 - AccountActivationsコントローラー

1. 現時点でテストスイートを実行するとgreenになることを確認してみましょう。

# rails test
Running via Spring preloader in process 13277
Started with run options --seed 33183

  43/43: [=================================] 100% Time: 00:00:15, Time: 00:00:15

Finished in 15.05852s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips

確かにテストは成功しますね。

2. 表 11.2の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。

ヒント: 私達はこれからメールで名前付きルートを使います。

「AccountActivationsリソースのeditアクションに対応するURLは、メールに記載するURLとして使われる」というのが前提となる、というのが大きなポイントです。

  • AccountActivationsリソースのeditアクションには、Railsアプリケーションの外部からアクセスできなければならない
  • Railsアプリケーションの外部からリソースにアクセスする場合、完全なURLが必要となる

このあたりが理由でしょうか。

AccountActivationのデータモデル

RDBには、有効化トークンをハッシュ化したものを保存する

「生の有効化トークンをRDBに保存する」という運用は、万が一RDBそのものの内容が漏洩した場合、容易に攻撃に悪用される脆弱な運用です。例えば、「攻撃者が新しく登録されたユーザーの有効化トークンを盗み取り、本来のユーザーが使う前に当該トークンを使ってしまう(そして当該ユーザーとしてログインしてしまう)」という攻撃が行われる危険性が想定できます。

というわけで、パスワードや記憶トークンと同様、有効化トークンについても、「RDBに保存するのはハッシュ化した値」という運用を行うこととします。

実装手法

実装手法は、節6.3におけるpassword仮想属性の実装、ならびに、節9.1におけるremember_token仮想属性の実装と類似しています。今回実装するのは、activation_tokenという仮想属性となります。

user.activation_token

最終的には、以下のようなコードでユーザーの有効化トークンを認証できるようになることを目指します。

user.authenticated?(:activation, token)

なお、authenticated?メソッドそのものの実装にも手を加えていくことになります。

Userモデルの実装内容を変更する

Userモデルには、以下3つの属性を新たに追加していくことになります。

  • activation_digest
    • string型
    • 有効化トークンに対するダイジェスト
  • activated
    • boolean型
    • ユーザーが有効化されたか否か
    • デフォルトではfalseである
  • activated_at属性
    • datetime型
    • ユーザーが有効化された日時

上記属性追加を反映した新たなUserモデルの全体像は、以下のようになります。

User_full.png

新たなUserモデルに対応するマイグレーション

まずはマイグレーションそのものを生成します。

root@705320d4d96d:/var/www/sample_app# rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
Running via Spring preloader in process 13329
      invoke  active_record
      create    db/migrate/[timestamp]_add_activation_to_users.rb

activated属性のデフォルト値をfalseとするため、生成されたマイグレーションを以下のように書き換えていきます。

db/migrate/[timestamp]_add_activation_to_users.rb
  class AddActivationToUsers < ActiveRecord::Migration[5.1]
    def change
      add_column :users, :activation_digest, :string
-     add_column :users, :activated, :boolean
+     add_column :users, :activated, :boolean, default: false
      add_column :users, :activated_at, :datetime
    end
  end

最後はrails db:migrateです。

# rails db:migrate
== [timestamp] AddActivationToUsers: migrating =============================
-- add_column(:users, :activation_digest, :string)
   -> 0.0169s
-- add_column(:users, :activated, :boolean, {:default=>false})
   -> 0.0015s
-- add_column(:users, :activated_at, :datetime)
   -> 0.0018s
== [timestamp] AddActivationToUsers: migrated (0.0205s) ====================

Active Recordのコールバックメソッドにより、ユーザーオブジェクト作成前に有効化トークン・有効化ダイジェストが生成されるようにする

before_createコールバック

「オブジェクトの新規作成時にのみ呼び出される」というコールバックです。引数として、メソッド名を表すシンボルを指定(あるいは実行する処理の内容をブロックとして直接記述)します。

今回は、「有効化トークン・有効化ダイジェストを生成する処理(create_activation_digestメソッドとします)」をbefore_createコールバックの対象とします。

before_create :create_activation_digest

create_activation_digestメソッド

有効化トークンと、有効化トークンに対応するダイジェストを生成する処理の実体を記述するメソッドです。

private

  def create_activation_digest
    self.activation_token = User.new_token
    self.activation_digest = User.digest(activation_token)
  end

privateメソッドとの関係

create_activation_digestの前にprivateメソッドが呼び出されているのは、一つの大きなポイントです。Userモデルのbeforeフィルターで呼び出されるメソッドは、Userモデル内でしか使わないので、Userクラス内でprivateメソッドを呼び出して以降に記述すべきとされています。そういえば、UsersコントローラーにStrong Parametersやbeforeフィルターを実装した際にもprivateメソッドを使いましたよね。

有効化トークンと、有効化トークンに対応するダイジェストを生成する処理の実体

表題記載の処理の実体は、以下2つの処理となります。

self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
User#rememberメソッドとの類似点と相違点

類似する処理として、永続cookiesに関する記憶トークンと記憶ダイジェストを生成するUser#rememberメソッドを、Railsチュートリアル本文の第9章で定義していました。

User#remember
def remember
  self.remember_token = User.new_token
  update_attribute(:remember_digest, User.digest(remember_token))
end

User#new_tokenメソッドの使い方は、rememberでもcreate_activation_digestでも同じです。

一方、User#digestメソッドの使い方は、remembercreate_activation_digestで異なります。

  • rememberにおけるUser#digestメソッドは、update_attributeメソッドの引数として呼び出している
    • すでにRDB上に存在するユーザー情報を対象とするため
  • create_activation_digestにおけるUser#digestメソッドは、実行結果をself.activation_digestに代入している
    • まだRDB上に存在しないユーザー情報を対象とするため
    • RDB上にユーザー情報が生成されるのは、create_activation_digestが呼び出されたである

Userモデルへの実装の追加・変更の全体像

Userモデルへの実装の追加・変更の全体像は、以下のようになります。なお、既存の「メールアドレスをすべて小文字にする」という処理も、downcase_emailというメソッドを呼び出す実装に変更しています。

app/models/user.rb
  class User < ApplicationRecord
-   attr_accessor :remember_token,
+   attr_accessor :remember_token, :activation_token
-   before_save { email.downcase! }
+   before_save :downcase_email
+   before_create :create_activation_digest
    ...略
+
+   private
+
+     # メールアドレスをすべて小文字にする
+     def downcase_email
+       self.email = email.downcase
+     end
+
+     # 有効化トークンとダイジェストを作成および代入する
+     def create_activation_digest
+       self.activation_token = User.new_token
+       self.activation_digest = User.digest(activation_token)
+     end
  end

この時点でテストは全て成功するはず

特に「RDBに格納されるデータにおいて、メールアドレスが全て小文字であるか」というテストが成功することは、再度確認が必要です。

test/models/user_test.rb(57行目)
test "email addresses should be saved as lower-case" do
# rails test test/models/user_test.rb:57
Running via Spring preloader in process 13346
Started with run options --seed 25681

  12/12: [=================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.20554s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

「RDBに格納されるデータにおいて、メールアドレスが全て小文字であるか」というテストは、無事成功しました。

続いて全体のテストを実行してみましょう。

# rails test
Running via Spring preloader in process 13361
Started with run options --seed 7529

  43/43: [=================================] 100% Time: 00:00:06, Time: 00:00:06

Finished in 7.00017s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips

こちらも無事成功しました。

サンプルユーザーの生成とテスト

サンプルユーザーを最初から有効にしておく

db/seeds.rb
  User.create(name:                 "Example User",
              email:                "example@railstutorial.org",
              password:             "foobar",
              password_confirmation: "foobar",
-             admin: true)
+             admin: true,
+             activated: true,
+             activated_at: Time.zone.now)

  99.times do |n|
    name = Faker::Name.name
    email = "example-#{n+1}@railstutorial.org"
    password = "password"
    User.create!( name:                  name,
                  email:                 email,
                  password:              password,
-                 password_confirmation: password)
+                 password_confirmation: password,
+                 activated: true,
+                 activated_at: Time.zone.now)
  end

fixtureのユーザーを最初から有効にしておく

test/fixtures/users.yml
  rhakurei:
    name: Reimu Hakurei
    email: rhakurei@example.com
    password_digest: <%= User.digest('password') %>
    admin: true
+   activated: true
+   activated_at: Time.zone.now

  mkirisame:
    name: Marisa Kirisame
    email: example.example@example.org
    password_digest: <%= User.digest('password') %>
+   activated: true
+   activated_at: Time.zone.now

  skomeiji:
    name: Satori Komeiji
    email: example_example@example.net
    password_digest: <%= User.digest('password') %>
+   activated: true
+   activated_at: Time.zone.now

  rusami:
    name: Renko Usami
    email: example0@example.com
    password_digest: <%= User.digest('password') %>
+   activated: true
+   activated_at: Time.zone.now

  <% 30.times do |n| %>
  user_<%= n %>:
    name:  <%= "User #{n}" %>
    email: <%= "user-#{n}@example.com" %>
    password_digest: <%= User.digest('password') %>
+   activated: true
+   activated_at: Time.zone.now
  <% end %>

サンプルデータを再度生成し直す

# rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
...略
== [timestamp] AddActivationToUsers: migrated (0.0165s) ====================

# rails db:seed
# 

「サンプルデータの入力におけるtypo」というのは案外やってしまいがちなので、この場面は結構緊張する場面です。今回は何事もなく完了しました。

演習 - AccountActivationのデータモデル

1. 本項での変更を加えた後、テストスイートがgreenのままになっていることを確認してみましょう。

# rails test
Running via Spring preloader in process 13392
Started with run options --seed 56186

  43/43: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.90673s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips

特にfixtureの変更時におけるtypoには注意が必要です。

2.1. コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると (Privateメソッドなので) NoMethodErrorが発生することを確認してみましょう。

# rails console --sandbox

>> user = User.first

>> user.create_activation_digest
Traceback (most recent call last):
        1: from (irb):2
NoMethodError (private method `create_activation_digest' called for #<User:0x00007fdd8823ae98>)
Did you mean?  restore_activation_digest!

2.2. また、そのUserオブジェクトからダイジェストの値も確認してみましょう。

>> user.activation_digest
=> "$2a$10$Q.bVywQrrgEJC6Mg0IhdXONY5M/0jQYm4/ZEBwhJfxcag7VBBx6S6"

3.1. downcase!メソッドを使って、リスト 11.3downcase_emailメソッドを改良してみてください。

リスト 6.34で、メールアドレスの小文字化にはemail.downcase!という (代入せずに済む) メソッドがあることを知りました。

User#downcase_email
  def downcase_email
-   self.email = email.downcase
+   self.email.downcase!   
  end

3.2. また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。

# rails test test/models/user_test.rb:57
Running via Spring preloader in process 13416
Started with run options --seed 43627

  12/12: [=================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.23544s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
# rails test
Running via Spring preloader in process 13429
Started with run options --seed 15928

  43/43: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.22996s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails+Vue.jsによるフォームの作例

私の体感では、Railsアプリケーションで開発にかかる時間の半分はテンプレートとJavaScriptで、その大半はフォームです。ややこしいテンプレートはRails側で頑張らずに、Vue.jsのようなJavaScriptのフレームワークに投げてしまう、という作り方を今後のスタイルとしたい。

このサンプルは、ここ1年半ほどRails上でVue.jsをいじくって考えた、現在のところのベターなパターンです。まだ研究中なところもあり、今後変更する可能性もあります。

サンプルプログラムはこちら。簡単なブログアプリケーションです。
https://github.com/kazubon/blog-rails6-vuejs

環境

  • Rails 5.2/6.0、Webpacker 4、Vue.js 2.6。
  • 非SPA、Turbolinksあり。
  • jQueryとBootstrapあり。

そこそこ大きな業務アプリケーションを想定(サンプルはブログですが)。Vue.jsではないJavaScriptを使っているなど、いろいろなスタイルのページが混じっているものとする。

ポイント

  • newとeditでは、2回リクエストを送る。1回目はふつうのHTMLで、ページの枠だけ受け取る。2回目はVueからAjaxでモデルのデータを受け取り、フォームの入力欄に反映する。
  • createとupdateは、Ajaxで呼ぶ。保存に成功したときはJavaScriptでリダイレクトし、失敗したらエラーメッセージを表示する。
  • 検索結果の表示ページでも、2回リクエストを送る。1回目はふつうのHTMLで、ページの枠だけ。2回目はVueからAjaxで検索のパラメータを送って記事リストを受け取り、一覧を表示する。

関連記事:

application.js

packs下のapplication.jsの書き方はいろいろ考えられますが、このサンプルではこんな感じです。HTML要素をid属性で探して、対応するVueアプリケーションをマウントします。

グローバル変数vuePropsは、Railsから直接データをVueのpropsに渡すのに使っています。

SessionForm(ログインフォーム、この記事では紹介なし)だけ.vueファイル内のテンプレートを使わずにRailsが出したHTMLをパースしていますが、これは比較研究したかっただけです。

app/javascript/packs/application.js
require("@rails/ujs").start();
require("turbolinks").start();

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

import EntryIndex from '../entries/index';
import EntryForm from '../entries/form';
import EntryStar from '../entries/star';
import SessionForm from '../sessions/form';

Vue.use(TurbolinksAdapter);

document.addEventListener('turbolinks:load', () => {
  let apps = [
    { elem: '#entry-index', object: EntryIndex },
    { elem: '#entry-form', object: EntryForm },
    { elem: '#entry-star', object: EntryStar },
    { elem: '#session-form', object: SessionForm }
  ];

  let props = window.vueProps || {};
  apps.forEach((app) => {
    if($(app.elem).length) {
      if(app.object.render) { // テンプレートあり
        new Vue({ el: app.elem, render: h => h(app.object, { props }) });
      }
      else { // HTMLをテンプレートに
        new Vue(app.object).$mount(app.elem);
      }
    }
  });
});

newとedit

編集ページのアクションです。HTMLの枠とAjaxでのデータ送信を同じアクションにしていますが、Ajax用を分けてapi/entries_controller.rbのような別コントローラにすることも考えられます。

Entries::Formは形式的に置いているもので、このサンプルの編集ページでは使ってません。

app/controllers/entries_controller.rb
  def new
    @entry = Entry.new
    @form = Entries::Form.new(current_user, @entry)
    respond_to do |format|
      format.html
      format.json { render :edit }
    end
  end

  def edit
    @entry = current_user.entries.find(params[:id])
    @form = Entries::Form.new(current_user, @entry)
    respond_to :html, :json
  end

編集ページのHTML枠です。editではグローバル変数vuePropsでEntryモデルのidを渡しています。

app/views/entries/new.html.erb
<div id="entry-form"></div>
app/views/entries/edit.html.erb
<script>
var vueProps = <%= { entryId: @entry.try(:id) }.to_json.html_safe %>;
</script>
<div id="entry-form"></div>

編集ページのフォーム用のVueです。createdでAjaxを使ってEntryモデルのデータを取得し、フォームにセットします。

app/javascript/entries/form.vue
<template>
  <div>
    <form @submit="submit">
      <div v-if="alert" class="alert alert-danger">{{alert}}</div>
      <div class="form-group">
        <label for="entry-title">タイトル</label>
        <input type="text" v-model="entry.title" id="entry-title"
          class="form-control" required maxlength="255" pattern=".*[^\s]+.*">
      </div>
      <div class="form-group">
        <label for="entry-body">本文</label>
        <textarea v-model="entry.body" id="entry-body" cols="80" rows="15"
          class="form-control" required maxlength="40000">
        </textarea>
      </div>
      <div class="form-group">
        <label for="entry-tag0">タグ</label>
        <div>
          <input v-for="(tag, index) in entry.tags" :key="index" v-model="tag.name"
            class="form-control width-auto d-inline-block mr-2" style="width: 17%"
            maxlength="255" >
        </div>
      </div>
      <div class="form-group">
        <label for="entry-published_at">日時</label>
        <input type="text" v-model="entry.published_at" id="entry-published_at"
          class="form-control"
          pattern="\d{4}(-|\/)\d{2}(-|\/)\d{2} +\d{2}:\d{2}">
      </div>
      <div class="form-group mb-4">
        <input type="checkbox" v-model="entry.draft" id="entry-draft" value="1">
        <label for="entry-draft">下書き</label>
      </div>
      <div class="row">
        <div class="col">
          <button type="submit" class="btn btn-outline-primary">{{entryId ? '更新' : '作成'}}</button>
        </div>
        <div class="col text-right" v-if="entryId">
          <button type="button" class="btn btn-outline-danger" @click="destroy">削除</button>
        </div>
      </div>
    </form>
  </div>
</template>

<script>
export default {
  props: ['entryId'],
  data() {
    return {
      entry: {},
      alert: null
    };
  },
  created() {
    axios.get(this.path() + '.json').then((res) => {
      this.entry = res.data.entry;
      this.initTags();
    });
  },
  methods: {
    path() {
      return this.entryId ? `/entries/${this.entryId}/edit` : '/entries/new';
    },
    initTags() {
      let len = this.entry.tags.length;
      if(len < 5) {
        for(let i = 0; i < 5 - len; i++) {
          this.entry.tags.push({ name: '' });
        }
      }
    },
// 中略
  }
}
</script>

newとeditのアクションで、Vueに渡すjsonデータです。

app/views/entries/edit.jbuilder
json.entry do
  json.title @entry.title
  json.body @entry.body
  json.published_at (@entry.published_at || Time.zone.now).strftime('%Y-%m-%d %H:%M')
  json.draft @entry.draft
  json.tags do
    json.array! @entry.tags do |tag|
      json.name tag.name
    end
  end
end

createとupdate

新規作成と更新のアクションです。成功したときは、jsonでリダイレクト先のパスを返します。失敗したときは、ステータスコード422とメッセージを返します。

ここで使っているEntries::Formは、バリデーションとデータの保存を行うものです。そのうち別記事で紹介します。

app/controllers/entries_controller.rb
  def create
    @entry = Entry.new
    @form = Entries::Form.new(current_user, @entry, entry_params)
    if @form.save
      flash.notice = '記事を作成しました。'
      render json: { location: entry_path(@entry) }
    else
      render json: { alert: '記事を作成できませんでした。' },
        status: :unprocessable_entity
    end
  end

  def update
    @entry = current_user.entries.find(params[:id])
    @form = Entries::Form.new(current_user, @entry, entry_params)
    if @form.save
      flash.notice = '記事を更新しました。'
      render json: { location: entry_path(@entry) }
    else
      render json: { alert: '記事を更新できませんでした。' },
        status: :unprocessable_entity
    end
  end
# 中略
  def entry_params
    params.require(:entry).permit(
      :title, :body, :published_at, :draft, tags: [ :name ]
    )
  end

編集ページのフォーム用のVueで、フォームを送信するメソッドです。送信時には、HTTPヘッダにmeta属性からCSRF対策のトークンを入れます。

成功したときは指定のパスにリダイレクトします。失敗したときはアラート表示部にメッセージを入れます。

実際のアプリケーションでは、トークンを指定する部分はコードを共通化するべきでしょう。レスポンスの処理部分も共通化したほうがいいかも。

app/javascript/entries/form.vue
<script>
export default {
// 中略
  methods: {
// 中略
    submitPath() {
      return this.entryId ? `/entries/${this.entryId}` : '/entries';
    },
    submit(evt) {
      evt.preventDefault();
      if(!this.validate()) {
        return;
      }
      axios({
        method: this.entryId ? 'patch' : 'post',
        url: this.submitPath() + '.json',
        headers: {
          'X-CSRF-Token' : $('meta[name="csrf-token"]').attr('content')
        },
        data: { entry: this.entry }
      }).then((res) => {
        Turbolinks.visit(res.data.location);
      }).catch((error) => {
        if(error.response.status == 422) {
          this.alert = error.response.data.alert;
        }
        else {
          this.alert = `${error.response.status} ${error.response.statusText}`;
        }
        window.scrollTo(0, 0);
      });
    },
// 中略
  }
}
</script>

index

検索フォームと記事一覧を表示するindexアクションです。ここでもHTMLの枠とAjaxのデータを同じアクションにしています。

検索のパラメータには、まとめて扱えるようにキーqを付けています。これは、まあ私の好みです。

ここで使っているEntries::SearchFormは、パラメータを使って検索を行うものです。これもそのうち別記事で紹介します。

app/controllers/entries_controller.rb
  def index
    @user = User.active.find(params[:user_id]) if params[:user_id].present?
    @form = Entries::SearchForm.new(current_user, @user, search_params)
    respond_to :html, :json
  end
(中略)
  def search_params
    return {} unless params.has_key?(:q)
    params.require(:q).permit(:title, :tag, :offset, :sort)
  end

検索ページのHTML枠の一部です。グロバール変数vuePropsでVueにユーザーID(ユーザー別一覧の場合)と検索パラメータを渡します。

app/views/entries/new.html.erb
<script>
var vueProps = <%= { userId: @user.try(:id), query: params[:q] || {} }.to_json.html_safe %>;
</script>
<div id="entry-index"></div>

検索フォームと記事一覧用のVueです。フォーム(search_form.vue)と一覧(list.vue)を子コンポーネントに分けています。

app/javascript/entries/index.vue
<template>
  <div>
    <search-form :userId="userId" :query="query"></search-form>
    <list :userId="userId" :query="query"></list>
  </div>
</template>

<script>
import Form from './search_form';
import List from './list';

export default {
  props: ['userId', 'query'],
  components: { 'search-form': Form, 'list': List }
}
</script>

検索フォームのVueです。ここでは特に何もしてません。タグを入力したときに動的に候補を出すような機能を付けるときはここに追加するつもりです。

検索フォームの送信はブラウザーに任せてページ遷移します。

app/javascript/entries/search_form.vue
<template>
  <div>
    <form action="/entries" method="get" class="form-inline mb-4">
      <input type="text" name="q[title]" class="form-control mr-3 mb-2"
        v-model="query.title" placeholder="タイトル">
      <input type="text" name="q[tag]" class="form-control mr-3 mb-2"
        v-model="query.tag" placeholder="タグ">
      <input type="hidden" name="q[sort]" v-model="query.sort">
      <input type="hidden" name="user_id" v-model="userId">
      <button type="submit" class="btn btn-outline-primary mb-2">検索</button>
    </form>
  </div>
</template>

<script>
export default {
  props: ['userId', 'query']
}
</script>

検索結果の一覧を出すVueです。createdでAjax送信を行い、記事一覧のデータを配列entriesに入れます。「もっと読む」ボタンを押したときは、オフセットを増やして記事を取得します。

app/javascript/entries/list.vue
<template>
  <div>
    <div class="text-right mb-3">
      {{entriesCount}}件 | 
      <a :href="sortPath('date')" v-if="query.sort == 'stars'">日付順</a>
      <template v-else>日付順</template>
      | <a :href="sortPath('stars')" v-if="query.sort != 'stars'">いいね順</a>
      <template v-else>いいね順</template>
    </div>
    <div class="entries mb-4">
      <div v-for="entry in entries" :key="entry.id" class="entry">
        <div>
          <a :href="entry.path">
            <template v-if="entry.draft">(下書き) </template>
            {{entry.title}}
          </a>
        </div>
        <div class="text-right text-secondary">
          <a :href="entry.user_path">{{entry.user_name}}</a> |
          <a v-for="tag in entry.tags" :key="tag.id" class="mr-2"
              :href="tag.tag_path">{{tag.name}}</a> |
          {{entry.published_at}} |
          <span class="text-warning" v-if="entry.stars_count > 0">★{{entry.stars_count}}</span>
        </div>
      </div>
    </div>
    <div v-if="showMore">
      <button type="button" @click="moreClicked" class="btn btn-outline-secondary w-100">もっと見る</button>
    </div>
  </div>
</template>

<script>
import axios from 'axios';
import qs from 'qs';

export default {
  props: ['userId', 'query'],
  data: function () {
    return {
      entries: [],
      entriesCount: 0,
      offset: 0
    };
  },
  computed: {
    showMore() {
      return (this.entries.length < this.entriesCount);
    }
  },
  created () {
    this.getEntries();
  },
  methods: {
    getEntries() {
      let params = { q: { ...this.query, offset: this.offset }, user_id: this.userId };
      let path = '/entries.json?' + qs.stringify(params);
      axios.get(path).then((res) => {
        this.entries = this.entries.concat(res.data.entries);
        this.entriesCount = res.data.entries_count;
      });
    },
    moreClicked() {
      this.offset += 20;
      this.getEntries();
    },
    sortPath(key) {
      let params = { q: { ...this.query, sort: key }, user_id: this.userId };
      return '/entries?' + qs.stringify(params);
    }
  }
}
</script>

indexアクションで、検索結果を返すjsonです。Entries::SearchFormのメソッドを呼び出しています。

app/views/entries/index.jbuilder
json.entries do
  json.array! @form.entries do |entry|
    json.id entry.id
    json.title entry.title
    json.path entry_path(entry)
    json.user_name entry.user.name
    json.user_path user_entries_path(entry.user)
    json.draft entry.draft?
    json.published_at entry.published_at.try(:strftime, '%Y-%m-%d %H:%M')
    json.stars_count entry.stars_count
    json.tags do
      json.array! entry.tags do |tag|
        json.id tag.id
        json.name tag.name
        json.tag_path(
          @user ? user_entries_path(@user, q: { tag: tag.name }) :
            entries_path(q: { tag: tag.name })
        )
      end
    end
  end
end
json.entries_count @form.entries_count

Vue.jsと関係ない補足

  • createとupdateは、Ajaxで呼ぶほうが楽です。失敗時にrender :newなどでテンプレートを作り直す手間が省けるのは大きい。また、失敗ページでのブラウザーの進む/戻る/リロードの動作が自然になります。
  • 検索フォームを送信するときは、GETメソッドでページ遷移させて、URLを変化させます。POSTメソッドを使ったりAjaxを使ったりすると、ブラウザーのリロードで検索結果が消えたり、検索結果をブックマークできなくなったりします。
  • JavaScriptでフォームの送信を扱うときは、送信ボタンのclickイベントじゃなくて、form要素のsubmitイベントを処理すること。

検討課題

  • Vueのテンプレートは.vueファイル内に入れるべきか、Railsが出力したHTMLを使うべきか。既存のアプリケーションでは、テンプレートをすべて.vueファイルに移すのは現実的に無理なので、両方ありとします。
  • フォームのAjax送信にrails-ujs(remote: trueを付けてajax:successイベントを処理)を使うべきか。rails-ujsでもいいんだけど、慣れていない人に分かりにくいのでAxiosで統一したほうがよい。
  • RailsからVueに直接データを渡す方法(このサンプルのグローバル変数vueProps)については、まだ迷い中。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

削除がうまくいかずエラーが出る。。。

ツイートを削除しようとしたら、こんなエラーが出た。

ActiveRecord::StatementInvalid in TweetsController#destroy

Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint fails

ダウンロード (6).png

僕はただツイートを削除したいだけなのに。。。


なぜエラーがでたのか

【結論】

tweetsテーブルcommentsテーブルで外部キー制約を結んでいたため。



【commentsテーブル】
21b53daebb42568420a0e4d42c965cf9.png

外部キーとして、tweet_idがあるのがわかります。

コメントは必ず何かしらのツイートに結びつきます。
勝手にツイートを削除すると、迷子のコメントが現れます!
これがエラーの正体です。


解決策

dependent: :destroyを追記。
ツイートを削除すると同時に、そのツイートに結びついたコメントも削除します。

app/models/tweet.rb
class Tweet < ApplicationRecord
  has_many :comments, dependent: :destroy
end



参考

https://qiita.com/kemako/items/e98174b9747e0b2c75a6



ではまた!

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

削除ボタンを押したら、確認ダイアログを出そう。

data: { confirm: "削除しますか?" }を追記

xxx.html.haml
= link_to "削除", path, method: :delete, data: { confirm: "削除しますか?" }

最後に加えればOK!
めっちゃ簡単に確認ダイアログが作成できた!嬉しい!!



ではまた!

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

redirect_backで直前のページにバック!

現在ツイート詳細ページ(show.html.haml)でコメントができます。
alt

ツイートにコメントをしたらコメントする前の状態に戻るようにします。
redirect_back(fallback_location: アクション名)で直前のページに戻すことができます。

app/controllers/comments_controller.rb
def create
  tweet = Tweet.find(params[:tweet_id])
  @comment = tweet.comments.build(comment_params)
  if @comment.save
    redirect_back(fallback_location: tweet_path(tweet))
  else
    flash[:danger] = "テキストを入力してください"
    redirect_back(fallback_location: tweet_path(tweet))
  end
end

tweet_pathはshowアクションのprefixです。

以上!



参考

https://railsguides.jp/layouts_and_rendering.html



ではまた!

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

ログイン中ユーザーの投稿内容をshowアクション以外で出力する方法

SNSっぽいアプリケーションのマイページで、ログイン中ユーザの投稿一覧を取得したかっただけなのにかなり詰まったので備忘録として残しておきます。

アプリの仕様上showアクションやshow.html.erbファイルを使用できず、index.html.erbで表示させるというレア?パターンではありますが。

環境

Rails 5.2.3
Ruby 2.5.1
gemのdeviseを使用
当記事で使用しているモデル名はplace

詰まった所

homes/index.html.erbファイル内にてeach doを利用し投稿内容を全部出力したい。

homes/index.html.erb
<% @myplaces.each do |myplace| %>
  #省略
<% end %>

全部のplaceを取得するのはhomesコントローラー内のindexアクションで

./controllers/homes_controller
@places = Place.all

こう書けばいい。そりゃそうだ。
問題はログイン中ユーザの投稿内容だけを取得したい時。

./controllers/homes_controller
@myplaces = Place.find(current_user.id)

と入力するとundefined method 'each' for nil:NilClassでエラーが発生する。これは@myplacesが空だからeachする元ネタがありませんよっていう怒られ方。できそうな気もするのに。

試しにターミナルでfind(1)として出力してみる。

ターミナル
pry(main)> Place.find(1)
   (0.5ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  Place Load (0.3ms)  SELECT  `places`.* FROM `places` WHERE `places`.`id` = 1 LIMIT 1
=> #<Place:0x00007f831ad85618
 id: 1,
 name: #以下省略

問題ない。
idが1であるcurrent_user.idを適当な変数に代入して出力してみる

./controllers/homes_controller
@user = current_user.id
homes/index.html.erb
<%= @user %>
#=> 1

こっちも問題がない。
なんで出力されないのか不思議で仕方がなかったんですが、驚いた事にこんな書き方だけで解決しました。

./controllers/homes_controller
@places = Place.all
places = @places
@myplaces = current_user.places
homes/index.html.erb
<% @myplaces.each do |myplace| %>
  #省略
<% end %>

目ン玉飛び出そうになりました。こんな簡単な記述でよかったんかと。。。普通にidを指定すれば出来るやろって余裕こいてたら痛い目見ました。

原因は今度時間を見つけて探してみます。今度作る時にはハマらないように…

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

【Rails】bundle install エラー「Errno::EACCES: Permission denied @ rb_sysopen -」の権限視点からの対応策 (mysql2)

備忘録です。
自身の記事ですが【Rails環境構築】MySQL2が原因で「bundle install」失敗した時の対処法のようなPATHの変更でも解決できない状況に陥ったため。

環境

OS       : Mac OS Catalina 10.15.1
Ruby      : v 2.6.3p62
Rails      : v 6.0.1
Homebrew  : v 2.1.16
Bundle    : v 1.17.2
MySQL : v 8.0.18 for osx10.15 on x86_64 (Homebrew)

その他
※ username は個人の作業ユーザ名になります。

$ whoami
username

エラー

$ bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installingfor. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
( ↑ bashの設定によって色が変わってたりするものの本稿でのエラーには関係なし)

省略

Installing mysql2 0.5.2 with native extensions
Errno::EACCES: Permission denied @ rb_sysopen -
/Users/username/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/mysql2-0.5.2/CHANGELOG.md
An error occurred while installing mysql2 (0.5.2), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  mysql2

対応

1. 上記エラーに記載されたディレクトリへ移動

cd /Users/username/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/extensions/x86_64-darwin-19/2.6.0/mysql2-0.5.2/

2. 該当ディレクトリでファイルの詳細を表示(権限確認)

$ ls -la
total 64
drwxr-xr-x  10 root   staff    320 12  2 22:08 .
drwxr-xr-x  69 username  staff   2208 12  2 22:08 ..
-rw-r--r--   1 root   staff     87 12  2 22:08 CHANGELOG.md
-rw-r--r--   1 root   staff   1078 12  2 22:08 LICENSE
-rw-r--r--   1 root   staff  24534 12  2 22:08 README.md
drwxr-xr-x   4 root   staff    128 12  2 22:08 examples
drwxr-xr-x   3 root   staff     96 12  2 22:08 ext
drwxr-xr-x   4 root   staff    128 12  2 22:08 lib
drwxr-xr-x  10 root   staff    320 12  2 22:08 spec
drwxr-xr-x   6 root   staff    192 12  2 22:08 support

3. ディレクトリやファイルの権限を 「root」 → 「username」 に変更&確認

$ sudo chown -R username .
$ ls -la
total 64
drwxr-xr-x  10 username  staff    320 12  2 22:08 .
drwxr-xr-x  69 username  staff   2208 12  2 22:08 ..
-rw-r--r--   1 username  staff     87 12  2 22:08 CHANGELOG.md
-rw-r--r--   1 username  staff   1078 12  2 22:08 LICENSE
-rw-r--r--   1 username  staff  24534 12  2 22:08 README.md
drwxr-xr-x   4 username  staff    128 12  2 22:08 examples
drwxr-xr-x   3 username  staff     96 12  2 22:08 ext
drwxr-xr-x   4 username  staff    128 12  2 22:08 lib
drwxr-xr-x  10 username  staff    320 12  2 22:08 spec
drwxr-xr-x   6 username  staff    192 12  2 22:08 support

変更されていればok

4. 「bundle install」(成功 or 再度エラーの対応)

恐らくここで完了する方もいれば、筆者のようにまたエラー発生する方もいるかと思います。
「うわ、また同じエラーだ😑」と感じるかもしれませんが、
エラーをよくよく見ると、gem内(1回目)とextensions内(2回目)で異なるので再度ディレクトリへ移動して権限変更を繰り返します。

$ bundle install

省略

Errno::EACCES: Permission denied @ rb_sysopen -
/Users/username/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/extensions/x86_64-darwin-19/2.6.0/mysql2-0.5.2/gem_make.out
An error occurred while installing mysql2 (0.5.2), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  mysql2
$ cd /Users/username/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/extensions/x86_64-darwin-19/2.6.0/mysql2-0.5.2/
$ ls -la
total 128
drwxr-xr-x   4 root   staff    128 12  2 22:08 .
drwxr-xr-x  12 username  staff    384 12  2 22:08 ..
-rw-r--r--   1 root   staff   1999 12  2 22:08 gem_make.out
-rw-r--r--   1 root   staff  58721 12  2 22:08 mkmf.log
$ sudo chown -R username .
$ ls -la
total 128
drwxr-xr-x   4 username  staff    128 12  2 22:08 .
drwxr-xr-x  12 username  staff    384 12  2 22:08 ..
-rw-r--r--   1 username  staff   1999 12  2 22:08 gem_make.out
-rw-r--r--   1 username  staff  58721 12  2 22:08 mkmf.log
$ bundle install

省略

Bundle complete! 17 Gemfile dependencies, 75 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

これにて終了です!

その他

誤解や「このやり方のが断然いい」などあれば、ご指摘頂けると幸いです m(_ _)m

参考記事

rails newでErrno::EACCES: Permission denied @ dir_s_mkdir -の対処法 - Qiita
Errno::EACCES: Permission denied @ dir_s_mkdir が出たので対処した - mimikunの技術メモ
lsコマンドの使い方と覚えたい15のオプション【Linuxコマンド集】

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

【Rails】 bundle install エラー「Errno::EACCES: Permission denied @ rb_sysopen -」の権限視点からの対応策 (mysql2)

備忘録です。
自身の記事ですが【Rails環境構築】MySQL2が原因で「bundle install」失敗した時の対処法のようなPATHの変更でも解決できない状況に陥ったため。

環境

OS       : Mac OS Catalina 10.15.1
Ruby      : v 2.6.3p62
Rails      : v 6.0.1
Homebrew  : v 2.1.16
Bundle    : v 1.17.2
MySQL    : v 8.0.18 for osx10.15 on x86_64 (Homebrew)

その他
※ username は個人の作業ユーザ名になります。

$ whoami
=> username

エラー

$ bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installingfor. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
( ↑ bashの設定によって色が変わってたりするものの本稿でのエラーには関係なし)

省略

Installing mysql2 0.5.2 with native extensions
Errno::EACCES: Permission denied @ rb_sysopen -
/Users/username/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/mysql2-0.5.2/CHANGELOG.md
An error occurred while installing mysql2 (0.5.2), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  mysql2

対応

1. 上記エラーに記載されたディレクトリへ移動

cd /Users/username/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/extensions/x86_64-darwin-19/2.6.0/mysql2-0.5.2/

2. 該当ディレクトリでファイルの詳細を表示(権限確認)

$ ls -la
total 64
drwxr-xr-x  10 root   staff    320 12  2 22:08 .
drwxr-xr-x  69 username  staff   2208 12  2 22:08 ..
-rw-r--r--   1 root   staff     87 12  2 22:08 CHANGELOG.md
-rw-r--r--   1 root   staff   1078 12  2 22:08 LICENSE
-rw-r--r--   1 root   staff  24534 12  2 22:08 README.md
drwxr-xr-x   4 root   staff    128 12  2 22:08 examples
drwxr-xr-x   3 root   staff     96 12  2 22:08 ext
drwxr-xr-x   4 root   staff    128 12  2 22:08 lib
drwxr-xr-x  10 root   staff    320 12  2 22:08 spec
drwxr-xr-x   6 root   staff    192 12  2 22:08 support

3. ディレクトリやファイルの権限を 「root」 → 「username」 に変更&確認

$ sudo chown -R username .
$ ls -la
total 64
drwxr-xr-x  10 username  staff    320 12  2 22:08 .
drwxr-xr-x  69 username  staff   2208 12  2 22:08 ..
-rw-r--r--   1 username  staff     87 12  2 22:08 CHANGELOG.md
-rw-r--r--   1 username  staff   1078 12  2 22:08 LICENSE
-rw-r--r--   1 username  staff  24534 12  2 22:08 README.md
drwxr-xr-x   4 username  staff    128 12  2 22:08 examples
drwxr-xr-x   3 username  staff     96 12  2 22:08 ext
drwxr-xr-x   4 username  staff    128 12  2 22:08 lib
drwxr-xr-x  10 username  staff    320 12  2 22:08 spec
drwxr-xr-x   6 username  staff    192 12  2 22:08 support

変更されていればok

4. 「bundle install」(成功 or 再度エラーの対応)

恐らくここで完了する方もいれば、筆者のようにまたエラー発生する方もいるかと思います。
「うわ、また同じエラーだ😑」と感じるかもしれませんが、
エラー先の場所をよくよく見ると、gem内(1回目)とextensions内(2回目)で異なるので再度ディレクトリへ移動して権限変更を繰り返します。

$ bundle install

省略

Errno::EACCES: Permission denied @ rb_sysopen -
/Users/username/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/extensions/x86_64-darwin-19/2.6.0/mysql2-0.5.2/gem_make.out
An error occurred while installing mysql2 (0.5.2), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  mysql2
$ cd /Users/username/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/extensions/x86_64-darwin-19/2.6.0/mysql2-0.5.2/
$ ls -la
total 128
drwxr-xr-x   4 root   staff    128 12  2 22:08 .
drwxr-xr-x  12 username  staff    384 12  2 22:08 ..
-rw-r--r--   1 root   staff   1999 12  2 22:08 gem_make.out
-rw-r--r--   1 root   staff  58721 12  2 22:08 mkmf.log
$ sudo chown -R username .
$ ls -la
total 128
drwxr-xr-x   4 username  staff    128 12  2 22:08 .
drwxr-xr-x  12 username  staff    384 12  2 22:08 ..
-rw-r--r--   1 username  staff   1999 12  2 22:08 gem_make.out
-rw-r--r--   1 username  staff  58721 12  2 22:08 mkmf.log
$ bundle install

省略

Bundle complete! 17 Gemfile dependencies, 75 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

これにて終了です!

その他

誤解や「このやり方のが断然いい」などあれば、ご指摘頂けると幸いです m(_ _)m

参考記事

rails newでErrno::EACCES: Permission denied @ dir_s_mkdir -の対処法 - Qiita
Errno::EACCES: Permission denied @ dir_s_mkdir が出たので対処した - mimikunの技術メモ
lsコマンドの使い方と覚えたい15のオプション【Linuxコマンド集】

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

Rails Tutorial Memo #5

自分用の備忘録です.

第10章 ユーザーの更新・表示・削除

この章で学べること

  • これまで未実装だったedit,update,index,destroyアクションを加え,RESTアクションを完成させる.
  • ユーザーが自分のプロフィールを自分で更新できるようにする.
  • すべてのユーザーを一覧できるようにする.
  • ユーザーを削除し,データベースから完全に消去する機能を追加する.

10.1 ユーザーを更新する

ユーザー情報を編集するパターンは新規ユーザーの作成と極めて似通っている.
newアクションと同じようにeditアクションを作成し,POST リクエストに応答するcreateアクションの代わりに PATCH リクエストに応答するupdateアクションを作成する.
ユーザーの登録は誰でも実行できるが,ユーザー情報を更新できるのはそのユーザー自身のみに限られる.

10.1.1 編集フォーム

ユーザー編集ページの正しい URL は/users/1/editであるため,ユーザーの id はparams[:id]で取り出すことができる.

Gravatarへのリンクでtarget="_blank"が使われているが,これを使うとリンク先を新しいタブ (またはウィンドウ) で開くようになるため,別のWebサイトへリンクするときなどに便利である(ただしtarget="_blank"にはセキュリティ上の小さな問題もある).

<a href="http://gravatar.com/emails" target="_blank">change</a>

一緒???

app/views/users/edit.html.erbform_for(@user)のコードは,app/views/users/new.html.erbform_for(@user)のコードと完全に同じ.

Rails はどうやって POST リクエストと PATCH リクエストを区別している?

A. ユーザーが新規なのか,それともデータベースに存在する既存のユーザーであるかを,Active Record の new_record?論理値メソッドを使って区別できるから.

Rails は@user.new_record?trueのときは POST を,falseのときは PATCH を使う.

target="_blank"の危険性

target="_blank"で新しいページを開くときには,セキュリティ上の小さな問題がある.それは,リンク先のサイトが HTML ドキュメントの window オブジェクトを扱えてしまう,という点である.具体的には,フィッシング (Phising) サイトのような,悪意のあるコンテンツを導入させられてしまう可能性がある.


対処法

リンク用のaタグのrel (relationship) 属性に、"noopener"と設定する

rel="noopener"の導入
<a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む