- 投稿日:2019-12-05T23:56:57+09:00
【Rails】ステータスフィード【Rails Tutorial 14章まとめ】
ステータスフィード
自身のマイクロポストと、フォローしているユーザーのマイクロポストを表示するフィードを実装する。
フィードの条件とテスト
フィードが満たす条件は以下の3つである。
①フォローしているユーザーのマイクロポストを含む
②自身のマイクロポストを含む
③フォローしていないユーザーのマイクロポストを含まない
これらを満たすように、まずテストを書く。test/models/user_test.rbrequire 'test_helper' class UserTest < ActiveSupport::TestCase . . . test "feed should have the right posts" do michael = users(:michael) archer = users(:archer) lana = users(:lana) # フォローしているユーザーの投稿を確認 lana.microposts.each do |post_following| assert michael.feed.include?(post_following) end # 自分自身の投稿を確認 michael.microposts.each do |post_self| assert michael.feed.include?(post_self) end # フォローしていないユーザーの投稿を確認 archer.microposts.each do |post_unfollowed| assert_not michael.feed.include?(post_unfollowed) end end endMichaelはarcherをフォローしており、lanaをフォローしていない設定である。
フィードの実装
条件を満たすフィードの実装には、自身のIDまたはフォローしているユーザーのIDに対応するuser_idを持つマイクロポストを取得する必要がある。
試作フィードでは自身のマイクロポストのみを表示していたので、feedメソッドは以下のようだった。app/models/user.rb# 試作feedの定義 def feed Micropost.where("user_id = ?", id) endフォローしているユーザーのマイクロポストを取得するために、まずフォローしているユーザーのIDを配列の形で取得する。
そのためには、following_idsメソッドを使う。>> User.first.following_ids => [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]following_idsメソッドは、has_many :followingの関連付けをした時に自動で生成される。
feedメソッドでこれを使うためには、SQLを使って以下のようにする。
app/models/user.rb# ユーザーのステータスフィードを返す def feed Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id) endこれでテストがGREENになる。
サブセレクトとフィードの改良
ここまでで実装したフィードではフォロー数が膨大になるとアプリケーションの動作が遅くなる。
フォローしているユーザーのIDをデータベースから取得した後、それを使ってまたデータベースからマイクロポストを取得するという操作をしていることが原因である。
この問題をSQLのサブセレクトを使用することで解決する。まずfeedメソッドを少し書き換える。
app/models/user.rb# ユーザーのステータスフィードを返す def feed Micropost.where("user_id IN (:following_ids) OR user_id = :user_id", following_ids: following_ids, user_id: id) endこのコードでは、変更前のコードで?に代入されていた部分がハッシュのキーと値に変わっている。
?にはキーが入っている。
これは同じ変数を複数の箇所で使用するためである。次にfollowing_idsを以下のようなSQLの文字列で置き換える。
following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id"user_idが1の場合、このコードは「ユーザー1がフォローしているユーザーすべてを選択する」という意味を持つ。
これによりフォローしているユーザーをIDを経由せずに直接データベースから取得できるため、動作を高速化できる。app/models/user.rb# ユーザーのステータスフィードを返す def feed following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id" Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id) endfollowing_idsにはSQLの文字列が入っているので、式展開を使っている。
- 投稿日:2019-12-05T23:47:35+09:00
個人開発の企画からリリース・運用まで
気づいたら2019年がもう終わりなのと、Qiitaのアイコンが変わっているのに最近気づきました。
今回はサービスを個人開発したときの流れを書いてみたいと思います。
何か作ってみたいけど
- 具体的に何を考えればいいのか
- どうやってリリースするのか
- どうやって改善していくのか
などの参考になればと思います。
TL;DR
- アイデアを出すには色々なサービスを自分で使ってみて、いいところや真似したいところを探す
- サービスを企画する上で最も重要な機能を1つ考えて、まずはそれを実現するために必要なことを書き出す
- 素早く作ってリリースしたいから
Rails
とHeroku
を使う- 見た目はそこそこでいいから
Bootstrap
で良さげなテンプレートを使う- サービスのフィードバックを素早く取り入れるためのCI/CD環境を整える
何故作るのか
個人開発サービスを作る理由は人それぞれだと思うのですが、個人的に以下の3点が多い気がします。
1. 勉強のために既存の似たようなサービスを作る
2. 収益のために作る
3. 自分が困っていることを解決するサービスを作る今回は3が理由でした。
もともと期間や時間が限られた中で人にプログラミングを教えるということをやっていて、BacklogやSlack、GoogleDriveを活用していたのですが、以下のような改善点を感じていました。
- 教えるために作る資料の作成に時間がかかる
- 作った資料がどれくらい理解されたか分からない
- 情報が一箇所にまとまってない
- 過去の資料を参考にしようとしても、「あれ?どこだっけ?」状態になる
- 自習しづらい
- マークダウンで資料を作れない
こういった改善点をなんとかできる既存のツールで目ぼしいものが無かったのと、自分で色々カスタマイズできたほうが良いなと思って、自分で作ることにしました。
コンセプトは、「これからプログラミングを勉強するための人に向けた管理ツール」にしました。
アイデア出し
作るコンセプトが決まったので、色々アイデアを出していきました。
- Googleでログインできる
- マークダウンで記事が書ける
- 記事を分類できる
- 権限によって見れる記事、見れない記事がある
- 見た記事、見てない記事が分かる
- 直近でやったことがタイムライン的に分かる
- 管理者がユーザーの状況を確認できる
- Slackに通知する
etc...
たくさんのアイデアを出すには、普段から色々なサービスを試してみながら、「あっ、これいいな!」をストックしていくのがおすすめです。
幾つかアイデアを出した後に、最も重要なものを絞り込みました。
今回は「マークダウンで記事が書ける」にしました。
最も重要なところから考えていく
さて、「マークダウンで記事が書ける」と決めたので、これを実現するために必要なことを考えました。
今回はRailsを使ったので、gemや参考記事も調べました。
https://qiita.com/hkengo/items/978ea1874cf7e07cdbfc
これでマークダウンで書けそうですが、これだけだと誰でも記事にアクセスできてしまうので、
ログイン機能も必要だと思いました。ログインはGoogleアカウントでログインさせたかったので、以下を参考にしました。
https://qiita.com/kenzoukenzou104809/items/c10a5642afbee93a961a
ということで、詳細なコードは省きますが、まず最初に「Googleアカウントでログインして、記事を書ける」ところまでを実装しました。
Herokuにデプロイ
デプロイはRailsと相性がよいHerokuを使いました。
AWSという選択肢もありましたが、素早く作りたかったのとインフラのメンテナンスコストを減らしたかったのでHerokuにしました。こちらの記事が参考になります。
Githubにpushしたら自動でデプロイさせる
プログラムを変更したら直ぐにデプロイされるようにしたかったので、Herokuの
Automatic Deploy
を使います。こちらが参考になります。
これで
- コミットしてGithubにプッシュ
- Herokuに自動デプロイ
- すぐ結果が分かる
の仕組みができました。
Bootstrapのテンプレートで見た目を整える
ここまでで、おおよその骨組みはできました。
あとは作り込んでいく感じです。まず見た目をある程度整えたかったので、Bootstrapのテンプレートを使うようにしました。
テンプレートを探す際は「Bootstrap テンプレート」や「Bootstrap admin template」などでググると色々なテンプレートが出てきます。
色々調べた結果、今回は以下を使いました。
https://www.creative-tim.com/product/now-ui-dashboard
テンプレートをダウンロードしてRailsに組み込む
テンプレートをダウンロードして解凍すると以下のようなフォルダ構成になってました。
これらをRailsに組み込むために以下のようなことをしました。
- examples配下のHTMLファイルをRailsの
public
配下にコピーする- Railsを起動させた状態で
localhost:3000/{移動したHTMLファイル名}
にアクセスして表示されるか確認する- ブラウザのコンソールにCSSやJS、画像ファイルがないというエラーが出ているので、必要なファイルをダウンロード元から
public
にコピーしていく- DEMOページと比較しながら、全てのページが問題なく表示されたら共通部分(ヘッダー・サイドメニュー・メインコンテンツの外枠)を特定する
- どのHTMLファイルでもいいので、共通部分をコピーして
application.html.haml
に組み込む(テンプレートエンジンはerbではなくhamlを使ってます)!!! %html %head %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ %title C-Arts %meta{:charset => "utf-8"}/ %meta{:content => "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0", :name => "viewport"}/ %meta{:content => "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible"}/ = csrf_meta_tags = csp_meta_tag = favicon_link_tag('favicon.ico') %link{:href => "https://fonts.googleapis.com/css?family=Montserrat:400,700,200", :rel => "stylesheet"}/ %link{:href => "https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css", :rel => "stylesheet"}/ = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' = javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %body .wrapper -# ここにサイドメニューのHTML -# xxxxxxxxxxxxxxxxxxx #main-panel.main-panel -# ここにヘッダーのHTML -# xxxxxxxxxxxxxxxxxxx -# 以下のyield部分がメインコンテンツになる = yield
- 動作確認しながらcssやjsを
public
配下からassets
配下に移動していくここまでで見た目の大枠ができたのであとはヘッダーやサイドメニューをカスタマイズしていきます。
そのままだとexamplesのページへのリンクになってたりするので、自分が必要なページへのリンクに変えていきます。また、今回使ったテンプレートにはログイン関係のHTMLが無かったので他のテンプレートからダウンロードしたものを、利用しました。
deviseを使ったのでログイン関連のページはこちらを参考にしてカスタマイズしました。あとは機能を作り込んでいく
見た目が整ったので、あとはひたすら機能を作ってリリースを目指しました。
UIを調整したいときは、公式ドキュメントを参考にしました。リリース後
- マークダウンでゴリゴリ資料を書いていく
- 誰が書いた、見た、見てないを一目で確認
- 資料作ったらSlackに通知
などなどメインとなる機能を使いながら、新しい機能を作ったらGithubにプッシュ->すぐにHerokuにデプロイって感じなので、
機能追加や改善も簡単にできます。
以上、簡単ではありますが個人開発したときの流れを書いてみました。
似たような方の参考になれば幸いです。それではみなさん良いお年を;)
- 投稿日:2019-12-05T23:38:36+09:00
【Rails】Rails⇔JavaScript間で時間データを渡す方法
【はじめに】
先日、こちらの記事を書きました。
【Rails】Rails側で定義した変数をJavaScriptに簡単に渡せるgem 「gon」を使ってみた - Qiitaただ、時間関係の変数についてはフォーマットがRubyとJavaScriptで異なってしまい、うまく動きません。
そこでRails⇔JavaScript間で時間データを渡す変換方法を調べたのでまとめてみました
【この記事が役に立つ方】
- Rails⇔JavaScript間で時間データをやりとりしたい方
【この記事のメリット】
- Rails⇔JavaScript間で時間データをやりとり出来るようになる。
【環境】
- macOS Catalina 10.15.1
- zsh: 5.7.1
- Ruby: 2.6.5
- Rails: 5.2.4
【ポイント】
1000を掛ける、割るで調整する。
【変換方法】
1.Ruby → JavaScript
t1 = Time.now => 2019-12-05 14:11:03 +0000 t1.to_f * 1000 => 1575555063351.479Ruby側で
to_f
メソッドを使い、1000を掛けることでミリ秒に変換。
↕
JavaScript側でnew Date()
の引数にそのミリ秒を渡す。let d1 = new Date(1575555063351.479) d1 // Thu Dec 05 2019 23:11:03 GMT+0900 (日本標準時)
2.JavaScript → Ruby
let d2 = new Date() d2 // Thu Dec 05 2019 23:20:04 GMT+0900 (日本標準時) d2.getTime() / 1000 // 1575555604.804JavaScript側で
getTime()
で拾った数値を1000で割る。
↕
Ruby側でTime.at()
の引数にその数値を渡す。Time.at(1575555604.804) => 2019-12-05 14:20:04 +0000【参考】公式リファレンスより
【Ruby】 Timeオブジェクト
秒
単位Time オブジェクトは時刻を起算時からの経過秒数で保持しています。起算時は協定世界時(UTC、もしくはその旧称から GMT とも表記されます) の 1970年1月1日午前0時です。なお、うるう秒を勘定するかどうかはシステムによります。
class Time (Ruby 2.6.0 リファレンスマニュアル)
【JavaScript】 Dateオブジェクト
ミリ秒
単位日付や時刻を扱うことが可能な、JavaScript の Date インスタンスを生成します。Date オブジェクトは、1970 年 1 月 1 日 (UTC) から始まるミリ秒単位の時刻値を基準としています。
【おわりに】
最後まで読んで頂きありがとうございました
特にJavaScriptと他言語は頻繁に関わってくると思いますが、言語間でエラーが出たらそもそも前提が違うという視点を持って対処していかないといけないですね
【参考にさせて頂いたサイト】(いつもありがとうございます)
- 投稿日:2019-12-05T22:36:05+09:00
roo gemを使ってExcelからデータを読み込む
こんにちは、マイナビでエンジニア兼マネージャーをしている柴垣と申します。
マイナビ学生の窓口というメディアを担当しています。きっかけ
マイナビ学生の窓口というメディアは大学生に対してキャリアのきっかけを届ける記事がたくさんあります。
ある日ディレクターから、「記事のカテゴリを更新して」というチケットが届きました。
対象の記事はidで指定されていて、現在のカテゴリ、変更後のカテゴリが記載されていました。
その数、約5,000!
最初は気づかずに、1行ずつ、手作業で、マッピング用のHashを作っていましたが、その数に気づいた時、これはあかん!となり、excelをcsvに変換してマッピング用のHashを作るtaskを作成しようと思っていました。
そこへ、あるアドバイスが
「xlsxなどから読み込んで処理するなら roo gem を使えば、ある程度自動化できそうです。」
ありがとうございます!rooを使う
rooとは
https://github.com/roo-rb/roo
Excelやその他類似のファイルを読み込むためのgemですインストール
githubのREADMEにあるようにGemfileに追記します
Gemfilegem 'roo', '~>2.8.0'bundle installします。
現時点では2.8.2がインストールされましたExcelファイルを読み込む
ではExcelファイルを読み込みます。読み込むExcelファイルを
new
の引数に指定します。load_excel.rakeexcel = Roo::Excelx.new('Excelのファイルパス')Sheetを指定する
Sheetの指定は先ほど作成した
Roo::Excelx
のインスタンスにsheet
メソッドで指定します。load_excel.rakesheet = excel.sheet('Sheet名')指定したカラムのデータを読み込む
Sheetを指定したら、データを読み込みます。
データを読み込むメソッドはいくつかあるようですが、わたしはparse
メソッドがオススメです。
ヘッダーも取り除かれます(必要な場合はheaders:true
を指定)、取得したいカラムの文字列を指定するだけです。
ExcelのSheetが以下のようになっていたとします。
id old_category_name new_category_name 1 カテゴリー1 カテゴリー2 2 カテゴリー1 カテゴリー3 そのときの指定方法は以下のようになります。
load_excel.rakerows = sheet.parse(id: 'id', old_cname: 'old_category_name', new_cname: 'new_category_name')rowsを出力することで正しく読み込めていることがわかります
load_excel.rakerows.each { |row| puts row.inspect } => {:id=>1, :old_cname=>"カテゴリー1", :new_cname=>"カテゴリー2"} {:id=>2, :old_cname=>"カテゴリー1", :new_cname=>"カテゴリー3"}とてもカンタンですね!
番外編
今回はカテゴリの更新ということで、oldとnewが同じ記事のIDは配列にまとめる必要がありました
例えば
id old_category_name new_category_name 1 カテゴリー1 カテゴリー2 2 カテゴリー1 カテゴリー3 3 カテゴリー1 カテゴリー2 4 カテゴリー1 カテゴリー2 5 カテゴリー1 カテゴリー3 となっていた場合、期待する結果は以下のようになります。
[ {old_cname: 'カテゴリー1', new_cname: 'カテゴリー2', ids: [1, 3, 4]}, {old_cname: 'カテゴリー1', new_cname: 'カテゴリー3', ids: [2, 5]} ]最初は、
Array#find
を使って、old_cname
,new_cname
が結果の配列に存在していたらids
に追加という処理にしていました。
すると、レビュー時にまた神の声が聞こえてきました。基本的にはコンテナオブジェクト(ArrayやHashなど)はHashのキーにしない方が良いのですが、ここは Array#find ではなく〜(略)
教えていただいた内容は
old_cname
とnew_cname
の配列をHashのkeyにして、そのvalueにidを突っ込んでいけばよいということでした。そのために、まずHashのデフォルト値を指定します
output = Hash.new{|h, k| h[k] = []}こうすることで、デフォルト値が
[]
になりました
あとはもう突っ込んでいくだけですrows.each do |row| output[[row[:old_cname], row[:new_cname]]] << row[:id] end ouptput => {["カテゴリー1", "カテゴリー2"]=>[1, 3, 4], ["カテゴリー1", "カテゴリー3"]=>[2, 5]}結果がHashになったことにより、カテゴリーの取得は少し工夫が必要で
output.each.each do |(old_cname, new_cname), ids| endとすることで
old_cname
,new_cname
,ids
それぞれの変数に代入ができます
- 投稿日:2019-12-05T22:25:07+09:00
Rails 初学者のアソシエーションの解説
has_manyとかいまいちわからない人向け!
これがわかる人は不要 Rails ガイド
1.has_manyとbelongs_toはセットではない。一緒に使うことが多いが独立して使うことができる。
2.メソッドを理解することが重要!例えば
Author(著者)テーブルとBook(本)テーブルがあったとし、その二つを関連づけたい時。class Author < ApplicationRecord
has_many :books, dependent: :destroy
endclass Book < ApplicationRecord
belongs_to :author
end上記の記述で下記のような連携ができるらしいのだが、いまいちどのような仕組みなのかわからなかった、、
@book = @author.books.create(published_at: Time.now)
こう言う時は一つづつ分解して考えるといい
authorはオブジェクト
booksはメソッド
createもメソッドcreateがメソッドなのはなんとなくわかるのだが、booksは??
もしhas_many: booksがなければメソッドにはならない。has_many: booksを追加したことによりbooksメソッドができた。ここでは表記されていないが実はhas_manyはbooks()だけでなく他のメソッドの使用も可能にしている。
そもそもメソッドってなんだっけ??
Rubyでよく見かけたこんな感じのやつです。引数を入れるとreturnで値を返す。def index @products = Product.all endbooksはこのようなメソッドであり、returnで返ってきた値が反映される。
なので整理するとauthor.books.createはbooksメソッドの引数がauthorと連携して、それにcreateメソッドが対応していると言うことです。
テキストに打ち込み説明するのは難しいですが、私自身も今理解しているところなので何かご指摘などございましたらよろしくお願いします。
- 投稿日:2019-12-05T21:24:00+09:00
#Rails + docker-compose + volume mount + dotenv gem の組み合わせで docker コンテナでは存在しないはずの環境変数が rails console や rspec で突如として現れキャッシュされているように思える怪奇現象と立ち向かっていた
Conclusion
- .env file copied from host and exist in docker container ( BAD CASE )
- "dotenv" gem read .env with rails console and rspec
- not exestence env in docker container appears in rails!
HEY DO NOT DO COPY FUZZY with Dockerfile
What was the trouble
is this the trouble dependent on my environment or code ?
.env
ABC=XXXdocker-compose up
.env file effect for build docker-compose file
it for not docker container ENVdocker container
Yes so not set ABC env in container
root@c0d7a2a32aec:/app# env | grep ABC
root@c0d7a2a32aec:/app#
but ...
Try rails console
oh whats happen!?
[1] pry(main)> ENV['ABC'] => "xxx"Try Ruby pry
there is not ENV['ABC']
it seems rails only trouble[1] pry(main)> ENV['ABC'] => nilI noticed
rails read .env file ?
this docker container BAD copy
copy all current directory filesCOPY . .So .env file is in docker container
root@432f3ef01b16:/app# cat .env ABC=xxxTry remove .env
And rerun rails console
root@349934df34c4:/app# rm .envspecify do not use spring cache
root@349934df34c4:/app# DISABLE_SPRING=1 bundle exec rails console [1] pry(main)> ENV['ABC'] => nil [2] pry(main)>ENV leave!
Reason
Gemfile
group :development, :test do gem "dotenv-rails"Ah I found maybe
Finally i tried
i remove
gem "dotenv-rails"
in Gemffile
bundle installand Try rails console
( and .env file is in docker container )
[2] pry(main)> ENV['ABC'] => "xxx"?
Conclusion
Do not copy to container .env file !
Original by Github issue
- 投稿日:2019-12-05T21:11:16+09:00
rails-tutorial第10章
modelだけ単数形の理由
modelは型に当たるので、それが複数形というのはちょっとということらしい。
edit updateアクションを実装しよう
まずはeditアクションをコントローラに実装
app/controllers/users_controller.rbclass UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end def edit @user = User.find(params[:id]) end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end endprivateについて
Rubyはクラスメソッドのオーバーライドなどもでき、外側からメソッドを変更される恐れがある。
そのため、privateを宣言しそれ以下に定義したメソッドに関しては外側からアクセスできないようにしている。これで勝手にadminユーザーを作られたりなどが防げる。編集フォームのviewを生成する。
app/views/users/edit.html.erb<% provide(:title, "Edit user") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Save changes", class: "btn btn-primary" %> <% end %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="http://gravatar.com/emails" target="_blank">change</a> </div> </div> </div>これはnewアクションのviewと同じなのに、なんでupdateアクションに飛ぶの??
これはnewの場合は全く新しいインスタンスなのに対して、editの場合は、すでにDBに保存されたもので中身もあるインスタンスだから。
この違いがform_forで生成されるhtmlに影響を与えているから。
$ rails console >> User.new.new_record? => true >> User.first.new_record? => falseこれは上記のようにrailsでtrueの時はpostリクエストを、falseの時はpatchリクエストを送るように判断するから。
updateアクションの実装
app/controllers/users_controller.rbclass UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end def edit @user = User.find(params[:id]) end def update @user = User.find(params[:id]) if @user.update_attributes(user_params) # 更新に成功した場合を扱う。 else render 'edit' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end endもう一度update_attributesメソッドを確認しよう
update_attributesとは、Ruby on Railsのモデルに備わるメソッドで、
モデルオブジェクト.update_attributes(キー: 値, キー: 値 … )
のようにHashを引数に渡してデータベースのレコードを複数同時に更新することができるメソッドです。データベースへの更新タイミングは、update_attributesメソッドの実行と同時で、validationも実行されます。
編集失敗時のテストを書こう
$ rails generate integration_test users_edit
test/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do get edit_user_path(@user) assert_template 'users/edit' patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } } assert_template 'users/edit' end end失敗時のコードは実装済みなのでテストは成功する。
次は編集に成功した時のテストを書こう
test/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "successful edit" do get edit_user_path(@user) assert_template 'users/edit' name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end end今の時点では成功時の処理を書いていないので、テストは失敗する。
@user.reloadはユーザーの情報をDBの情報と同期するというもの。
具体的にupdateアクションを実装する
app/controllers/users_controller.rbclass UsersController < ApplicationController . . . def update @user = User.find(params[:id]) if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . endflashとredirectを実装したので、テストが通りそうに思えるが、これでもまだ失敗してしまう。
それはテストコードで書いたパスワードが空のためだ。パスワードには、空ではなく、さらに6文字以上というバリデーションを設定しているため、それに引っかかってしまったのである。
この状態だと、名前とemailを変えるだけなのに、passwordを毎回入力しなければいけなくなってしまう。
これを解決するにはallow_nil: trueを指定してあげると良い。
app/models/user.rbclass User < ApplicationRecord attr_accessor :remember_token before_save { self.email = email.downcase } 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 . . . endhas_secure_passwordでは (追加したバリデーションとは別に) オブジェクト生成時に存在性を検証するようになっているため、空のパスワード (nil) が新規ユーザー登録時に有効になることはありません。(空のパスワードを入力すると存在性のバリデーションとhas_secure_passwordによるバリデーションがそれぞれ実行され、2つの同じエラーメッセージが表示されるというバグがありましたが (7.3.3)、これで解決できました。)
つまり、allow_nilを指定してもオブジェクト生成時にはhas_secure_passwordで存在性のバリデーションをしてくれるので安心である。
これでテストが通る。
認可について
今の状態だと、url直打ちでログインしてないのにuser_pathに入れたりなどが起こってしまう。
そのため、適切なユーザーでないとそのページにアクセスできないようにしたい。
beforeフィルターを使ってユーザーログインを要求する。
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end endbefore_action :logged_in_user, only: [:edit, :update]
これは userコントローラのeditメソッドとupdateメソッドを呼び出す前にlogged_in_userメソッドを呼び出してねーって意味。
ここで、createアクションを直打ちでしたらエラーになるんじゃね?って思うでしょ?
でも、resourcesで /usersになっているため直打ちしてもindexアクション呼び出してーってなるから大丈夫。
いや、でもshowアクションなんかは直打ちでアクセスできちゃうぞ?
これどうするんだ?beforeフィルターをした時の注意点
before_フィルターをすると、今まで通っていたテストが失敗してしまう。
理由はテスト時にedit,updateアクションを呼び出すときにログインを要求するようになったからだ。これを解決するには、テスト時のユーザーに事前にログインさせる必要がある。
test/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do log_in_as(@user) get edit_user_path(@user) . . . end test "successful edit" do log_in_as(@user) get edit_user_path(@user) . . . end endこれでテストは通るようになる。
ただ、今度はbefore_actionが万が一コメントアウトされたときにテストで検知しないといけなくなってしまった。
これを解決していこう。
これはuser_controller_testに書いていく。
test/controllers/users_controller_test.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "should redirect edit when not logged in" do get edit_user_path(@user) assert_not flash.empty? assert_redirected_to login_url end test "should redirect update when not logged in" do patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert_not flash.empty? assert_redirected_to login_url end endこのテストはログインしてない状態でedit,updateアクションを呼び出そうとすると、フラッシュメッセージが表示され、さらにログインページにリダイレクトされてますよね?ってテスト。
これを書いておくことで、万が一before_actionが消されるとテストが失敗するようになるので安心だよねーって話。
ただ、今の状態だと、ログインしてさえいれば他人の情報を変更できるという状態。
これを解決するにはどうすれば良いか?このテストを書くにあたって、2人以上のサンプルデータがないとダメなので、fixtureにデータをたす
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %>次にコントローラの単体テストを書く
test/controllers/users_controller_test.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end endこの状態だとテストが落ちてしまう。
じゃあどうすれば?
before_actionを追加しよう
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] . . . def edit end def update if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user end endここで気をつけなければいけないのは、
before_actionには順序があるということだ。上から順番に実行されるので、ログインしている、かつ、正しいユーザーか?という順序で実行させる。
また、def logged_in_userでcurrent_userがいることは確定しているので、
無駄な処理を書かずに、params[:id]から取得したユーザーとcurrent_userを比較すれば良いこれでテストは通る。
一応リファクタリングもしておこう
unless @user == current_user
この部分を綺麗にしたい。
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーをログイン def log_in(user) session[:user_id] = user.id end # 永続セッションとしてユーザーを記憶する def remember(user) user.remember cookies.permanent.signed[:user_id] = user.id cookies.permanent[:remember_token] = user.remember_token end # 渡されたユーザーがログイン済みユーザーであればtrueを返す def current_user?(user) user == current_user end # 記憶トークン (cookie) に対応するユーザーを返す def current_user . . . end . . . endヘルパーメソッドを定義したので、
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] . . . def edit end def update if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end endredirect_to(root_url) unless current_user?(@user)というように書き換えることができる。
フレンドリーフォアーディング
親切なリダイレクトを作ろう、もともとアクセスしたかったページにリダイレクトしてあげるようにしようということ。
まずは統合テストを書いていこう。
test/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "successful edit with friendly forwarding" do get edit_user_path(@user) log_in_as(@user) assert_redirected_to edit_user_url(@user) name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end endこのようにuser_editが成功するストーリーに追加してあげる。
このままだと、ログイン後userページに飛んでしまうため、テストは失敗する。次は、
ユーザーがもともと行きたかったページを覚えていたらそのページにアクセスし、覚えていなかったら普通のユーザーページにアクセスできるようなヘルパーメソッドを定義する。
sessionを使うと便利。
app/helpers/sessions_helper.rbmodule SessionsHelper . . . # 記憶したURL (もしくはデフォルト値) にリダイレクト def redirect_back_or(default) redirect_to(session[:forwarding_url] || default) session.delete(:forwarding_url) end # アクセスしようとしたURLを覚えておく def store_location session[:forwarding_url] = request.original_url if request.get? end endrequest.original_urlについて見ていこう。
requestはもともとある特殊な変数である。
original_urlメソッドを渡してあげると、userがもともと行きたかったurlを参照することができる。if request.get?について見ていこう。
本来ログインせずにpatchリクエストをしてupdateアクションを呼び出すのは意味がない。
なので、if request.get?でgetリクエストだった時だけ、session[:forwarding_url]に情報を格納するよーって話。もともと行きたかったurlが発生するのは必ずログイン前のことなので、
store_locationメソッドをbefore_actionで指定したメソッドの中に記載する。app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] . . . def edit end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? store_location flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end endもしログインしてなかったら、store_locationメソッドが呼び出されて、もともと行きたかったurlが参照できる。
そして、redirect_back_orメソッドをsessionコントローラのcreateメソッドの中に入れる。
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController . . . def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_back_or user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end . . . end本当はここでテスト通るはずなんだが、まさかの失敗。
NameError: NameError: undefined local variable or method ` store_location' for #<UsersController:0x0000000006837bc8> Did you mean? store_location app/controllers/users_controller.rb:51:in `logged_in_user' test/integration/users_edit_test.rb:22:in `block in <class:UsersEditTest>'このようにエラーが出た。
で、これは全角スペースがcontrollerの方に含まれていたからなんだね。
全角スペースもrubyではメソッド名に入る。
なのでこれを取り除くとテストが通った。全てのユーザーを表示する
まずはログインしてないユーザーがindexアクションをリクエストしたときにログインページに飛ぶかをテストする。
test/controllers/users_controller_test.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end test "should redirect index when not logged in" do get users_path assert_redirected_to login_url end . . . endこの状態だとテストは失敗してしまう。
テストを通るようにするのは簡単。users_controllerにindexアクションを定義して、before_actionにindexアクションを追加してあげればいいだけ。
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] before_action :correct_user, only: [:edit, :update] def index end def show @user = User.find(params[:id]) end . . . end次にindexアクションに対応するviewを作っていこう。
app/views/users/index.html.erb<% provide(:title, 'All users') %> <h1>All users</h1> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul>ただ、これだと、gravatar_forメソッドに引数が2つ渡されている。
users_helperに定義したメソッドは引数が1つしか渡せない。これを直す必要がある。
app/helpers/users_helper.rbmodule UsersHelper # 渡されたユーザーのGravatar画像を返す def gravatar_for(user, options = { size: 80 }) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) size = options[:size] gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end endこれでエラーはなくなるが、cssが整ってないので、
app/assets/stylesheets/custom.scss/* Users index */ .users { list-style: none; margin: 0; li { overflow: auto; padding: 10px 0; border-bottom: 1px solid $gray-lighter; } }それが終わったらheaderにリンクを設定する。
サンプルユーザーの作成
ページネーションを試したいが、30人手動で作成するのはかなり面倒。
なのでコンピュータに作ってもらおう。まずはGemfileにFaker gemを追加します
source 'https://rubygems.org' gem 'rails', '5.1.6' gem 'bcrypt', '3.1.12' gem 'faker', '1.7.3'bundleをする
データベース上にサンプルユーザーを生成するRailsタスク
次にサンプルユーザーの生成するためのコードを書く。
db/seeds.rbUser.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar") 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) end注目すべきはUser.create!
db/seedsはべきとうせいではないので、一度実行して、2度目を実行しようとすると、再度100人サンプルユーザーが作られてしまう。
ただ、メールアドレスはユニークネスのバリデーションを設定しているので、100回間違えることになる。
これを避けるために、!を追加して、1人目の時点でバリデーションに引っかかったら例外を出すようにしている。
faker gemの機能を試してみよう
$ rails db:migrate:reset
このコマンドを実行すると、開発用のDBのデータをリセットすることができる。そして、
$ rails db:seed
このコマンドを実行すると、サンプルユーザーが100人作られる。ページネーション
サンプルユーザーが100人できたので、ページネーション機能を作っていこう
source 'https://rubygems.org' gem 'rails', '5.1.6' gem 'bcrypt', '3.1.12' gem 'faker', '1.7.3' gem 'will_paginate', '3.1.6' gem 'bootstrap-will_paginate', '1.0.0'will_pagenateというgemをインストールする。
gem 'bootstrap-will_paginate', '1.0.0'はページネーション機能とbootstrapを関連つける
pagenationを表示させる
pagenationを表示させるのは簡単で、
app/views/users/index.html.erb<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul> <%= will_paginate %>pagenationを表示させたいところで<%= will_paginate %>と書くだけで良い。
ここで少し注意点
<%= will_paginate %>は本来<%= will_paginate @users %>となる。
今いるコンテキストの扱っているリソースを推測してくれて、pagenateする機能なのである。
これだけでは、ダメらしい。
具体的には、indexアクション内のallをpaginateメソッドに置き換えます (リスト 10.46)。ここで:pageパラメーターにはparams[:page]が使われていますが、これはwill_paginateによって自動的に生成されます。
app/controllers/users_controller.rbclassUsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] . . . def index @users = User.paginate(page: params[:page]) end . . . end実装はこれで終わり。
pagenationテストを行う
pagenationのテストを行うにはテスト環境にユーザーが30人以上いないとダメ。
それを解決するには、
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %>というようにしてあげる。
統合テストのファイルを作成しよう
$ rails generate integration_test users_index
test/integration/users_index_test.rbrequire 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "index including pagination" do log_in_as(@user) get users_path assert_template 'users/index' assert_select 'div.pagination' User.paginate(page: 1).each do |user| assert_select 'a[href=?]', user_path(user), text: user.name end end end今回のテストでは、paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認します。
30人以上のユーザーがいるとpaginationクラスを持つようになるらしい。
indexアクションのviewをリファクタリングしよう
app/views/users/index.html.erb<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <%= render user %> <% end %> </ul> <%= will_paginate %>ここでは、renderをパーシャル (ファイル名の文字列) に対してではなく、Userクラスのuser変数に対して実行している点に注目してください。この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、このパーシャルを作成する必要があります
app/views/users/_user.html.erb<li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li>これはさらにリファクタリングができる。
app/views/users/index.html.erb<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <%= render @users %> </ul> <%= will_paginate %><%= render @users %>をすると、railsが自動的に
<% @users.each do |user| %> <%= render user %> <% end %>のコードを展開してくれる。
なので、これを分かっていれば、
パーシャルを先に用意して、あとはrender @usersとすれば簡単にユーザーの情報を並べて表示することができる。この状態でテストも通る。
このリファクタリングはかなりショートカットできるので覚えるように!!!
ユーザーを削除する
管理権限を持っていればユーザーを削除できるようにする。
まずは、
特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加します。この後で説明しますが、こうすると自動的にadmin?メソッド (論理値を返す) も使えるようになりますので、これを使って管理ユーザーの状態をテストできます。
$ rails generate migration add_admin_to_users admin:boolean
今回は作成されたマイグレーションファイルにデフォルト値を設定する
db/migrate/[timestamp]_add_admin_to_users.rbclass AddAdminToUsers < ActiveRecord::Migration[5.0] def change add_column :users, :admin, :boolean, default: false end endbooleanなのでtrueかfalseかしか入らないと思いがちだが、どうやらnilも入るらしい。
なので、あらかじめdefaultオプションでfalseを指定しておく。adminユーザーを作ってみよう
db/seeds.rbUser.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true) 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) endそして、データベースをリセットして再度サンプルデータをページネーションで表示してあげる。
$ rails db:migrate:reset
$ rails db:seeddestroyアクションを実装しよう
まずはadminユーザーだけリンクが見えるようにしよう
app/views/users/_user.html.erb<li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> <% if current_user.admin? && !current_user?(user) %> | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %> <% end %> </li>userはuser_path(user)の略。
destroyアクションを作る
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] . . . def destroy User.find(params[:id]).destroy flash[:success] = "User deleted" redirect_to users_url end private . . . endこれだけではダメ、
ログインしていて、かつadminユーザーじゃないとだめapp/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] before_action :admin_user, only: :destroy . . . private . . . # 管理者かどうか確認 def admin_user redirect_to(root_url) unless current_user.admin? end endユーザーの削除のテスト
まずはテスト用のサンプルユーザの中にadminユーザーを作る。
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: trueテストコードを書く。
以下は、adminユーザーだけどログインしてない、そして、ログインしてるけどadminユーザーじゃない場合はどちらもdestroyアクション使えないよねーってテストtest/controllers/users_controller_test.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end . . . test "should redirect destroy when not logged in" do assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to login_url end test "should redirect destroy when logged in as a non-admin" do log_in_as(@other_user) assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to root_url end end統合テストも書いてみる。
test/integration/users_index_test.rbrequire 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @admin = users(:michael) @non_admin = users(:archer) end test "index as admin including pagination and delete links" do log_in_as(@admin) get users_path assert_template 'users/index' assert_select 'div.pagination' first_page_of_users = User.paginate(page: 1) first_page_of_users.each do |user| assert_select 'a[href=?]', user_path(user), text: user.name unless user == @admin assert_select 'a[href=?]', user_path(user), text: 'delete' end end assert_difference 'User.count', -1 do delete user_path(@non_admin) end end test "index as non-admin" do log_in_as(@non_admin) get users_path assert_select 'a', text: 'delete', count: 0 end endunless user == @admin assert_select 'a[href=?]', user_path(user), text: 'delete' endこれは、@adminとuserが異なる場合、deleteのリンクが見えるよね?というテスト
逆に@admin = userの場合、deleteリンク見えませんよねーってことassert_difference 'User.count', -1 do delete user_path(@non_admin) endまた、do endでUserの数が1少なくなることを表す時は-1を指定していることにも注目。
herokuのDBを初期化する
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
このコマンドって、pushしてからじゃないと、マイグレーションファイルがそもそもないので意味がないってことなんじゃない?桜を作る
$ heroku run rails db:seed
$ heroku restart
- 投稿日:2019-12-05T19:45:27+09:00
モデルとデータベースの関係
MVCモデルのM(モデル)についてまとめます。
モデルとは何か?
モデルの一番の役割は、Railsの中でデータベースへのアクセスをはじめとする情報のやりとりに関する処理を担当しています。
データベースとテーブルとは何か?
データベースはデータを保存する場所です。
テーブルはそれぞれのデータを整理し、振り分けたものです。
データベースはデータを保存するための箱、テーブルは収納場所でイメージすると少しわかりやすいと思います。モデルは仕分け人、テーブルは引き出しのイメージです。
必要なデータの登録やデータの引き出しをモデルが担当します。テーブルとモデルの関係性は、テーブルとモデルクラスの名前によって決定します。
これは、モデルの命名規則と関係しています。
モデルとテーブルを紐づけることでデータの登録、取り出しができます。
例)userモデル⇄userテーブルまとめ
モデルとデータベースの間では、モデルを通してデータベースにアクセスしてデータのやりとりをするためにあります。
- 投稿日:2019-12-05T19:24:13+09:00
Rails 6.0の "Active Storage's ImageProcessing transformer doesn't support :combine_options" という警告に対処する方法
困っていたこと
Rails 5.2時代にActiveStorageでこんなコードを書いていました。
user.avatar.variant( combine_options: { resize: "150^", gravity: "Center", crop: "150x150+0+0", auto_orient: true } )やっていることは縦長だったり横長だったりする画像を、中心で正方形に切り抜いて150x150サイズに縮小することです。(mini_magickを使用)
このコードをRails 6.0に上げるとこんな警告が出ました。
DEPRECATION WARNING: Generating image variants will require the image_processing gem in Rails 6.1. Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.なにやらimage_processing gemをインストールしろ、と言われているので、言われたとおりGemfileに追加します。
Gemfilegem 'image_processing', '~> 1.2'bundle install
すると、今度は警告の内容が次のように変わりました。
DEPRECATION WARNING: Active Storage's ImageProcessing transformer doesn't support :combine_options, as it always generates a single ImageMagick command. Passing :combine_options will not be supported in Rails 6.1.こちらの警告は
:combine_options
はRails 6.1でサポートされなくなるぜ!と言われてるものの、具体的な修正方法がわかりません。解決方法
この警告が導入されたコミットを確認した結果、どうやら
:combine_options
をなくしてこう書けば良いっぽいです。user.avatar.variant( resize: "150^", gravity: "Center", crop: "150x150+0+0", auto_orient: true )これで警告が消え、画像の切り抜きもうまくいきました。
ただ、これがベストプラクティスかどうかは確信がないので、もっと良い解決策を知ってる人がいたらコメントお待ちしています!
追記:こんなやり方があった!
同僚の @aki77 が「こんなやり方がありますよー」と、もっと便利な書き方を教えてくれました。(どうもありがとう!)
確かもうちょっと簡単に書けるようになったはずと思って調べてみました。
多分↓で良さそう。user.avatar.variant(resize_to_fill: [150, 150])
https://edgeguides.rubyonrails.org/active_storage_overview.html#transforming-imagescarrierwaveでも同じだったなーと思ったら、あっちもimage_processing使ってるんですね。
https://github.com/carrierwaveuploader/carrierwave/blob/master/carrierwave.gemspec#L27ということで、Rails 6ではこれだけでOKです!
(auto_orient: true
もデフォルトでセットされます)user.avatar.variant(resize_to_fill: [150, 150])めっちゃシンプルになりましたね!やったー?
- 投稿日:2019-12-05T18:01:39+09:00
Railsチュートリアル 第11章 アカウントの有効化 - アカウント有効化のメール送信
何をするか
「RailsのAction Mailerライブラリを使って、Userのメイラーを追加する」というのがこの節の内容となります。
以下がポイントとなります。
- 今回実装するメイラーは、Usersコントローラーの
create
アクションにおいて、有効化リンクをメール送信するために用いる- メイラーの構成は、コントローラーのアクションと類似している
- メールのテンプレートは、ビューと同様の要領で定義できる
- 今回実装するテンプレートには、有効化トークンとメールアドレス(=有効にするアカウントのメールアドレス)を含む
送信メールのテンプレート
メイラーの生成
メイラーの生成は、モデルやコントローラー等と同様、
rails generate
で行います。# rails generate mailer UserMailer account_activation password_reset Running via Spring preloader in process 13442 create app/mailers/user_mailer.rb invoke erb create app/views/user_mailer create app/views/user_mailer/account_activation.text.erb create app/views/user_mailer/account_activation.html.erb create app/views/user_mailer/password_reset.text.erb create app/views/user_mailer/password_reset.html.erb invoke test_unit create test/mailers/user_mailer_test.rb create test/mailers/previews/user_mailer_preview.rb今回実装するのは
account_activation
メイラーのほうです。password_reset
メイラーは、Railsチュートリアルの次章「第12章 パスワードの再設定」で用いるためのメイラーとなります。1つのメイラーに対し、2つのビューのテンプレートが生成される
例えば
account_activation
という1つのメイラーに対し、ビューのテンプレートは、以下の2つ生成されています。
account_activation.text.erb
account_activation.html.erb
.text.erb
のほうはテキストメール用、.html.erb
のほうはHTMLメール用のビューのテンプレートとなります。生成されたメイラーの内容
ビューのテンプレート
app/views/user_mailer/account_activation.text.erb(テキストメール用のテンプレートの初期状態)User#account_activation <%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erbapp/views/user_mailer/account_activation.html.erb(HTMLメール用のテンプレートの初期状態)<h1>User#account_activation</h1> <p> <%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb </p>メイラーのテンプレート
app/mailers/application_mailer.rbclass ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' end
app/mailers/application_mailer.rb
には、アプリケーション全体で共通する実装が記述されています。コントローラーにおけるapp/controllers/application_controller.rb
に相当します。
- デフォルトの
from
アドレス- デフォルトで使われるメイラーのレイアウト
なお、メイラーのレイアウトは、
app/views/layouts
以下に、テキストメール(mailer.text.erb
)・HTMLメール(mailer.html.erb
)それぞれで定義されています。app/mailers/user_mailer.rbclass UserMailer < ApplicationMailer # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.user_mailer.account_activation.subject # def account_activation @greeting = "Hi" mail to: "to@example.org" end # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.user_mailer.password_reset.subject # def password_reset @greeting = "Hi" mail to: "to@example.org" end end
app/mailers/user_mailer.rb
は、実際に「アカウントの有効化」や「パスワードの再発行」に際してメールを送信するための動作を実装していくコードです。今回であれば、account_activation
に対して実装を追加していく、というところですね。メイラーのインスタンス変数
メイラーの実装に、
@greeting
というインスタンス変数が登場しています。@greeting
に限らず、メイラー内で使われるインスタンス変数は、そのままメイラービューでも使うことができます。「コントローラー内で使われるインスタンス変数を、対応するビューで使うことができる」のと同じような感じですね。メイラーの実装を変更する
今回の実装内容は以下です。
- fromアドレスのデフォルト値を変更する
- アカウント有効化リンクをメール送信できるようにする
fromアドレスのデフォルト値を変更する
fromアドレスのデフォルト値は、
app/mailers/application_mailer.rb
から変更します。app/mailers/application_mailer.rbclass ApplicationMailer < ActionMailer::Base - default from: 'from@example.com' + default from: "noreply@example.com" layout 'mailer' endアカウント有効化リンクをメール送信できるようにする
アカウント有効化リンクをメール送信する一連のプロセスは、
app/mailers/user_mailer.rb
に実装していきます。app/mailers/user_mailer.rbclass UserMailer < ApplicationMailer def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end #...略 end
account_activation
は1つの引数を取る
account_activation
メソッド内部では、当該引数をuser
という名前で用いる- Userモデルのインスタンス
@user
というインスタンス変数を定義している
user
をその値とする@user
は、account_activation.*.erb
ビューで使うことになる- メールの宛先は
user
の- メールのタイトルは「Account Activation」とする
テンプレートビューを実装する
edit_account_activation_url
なるアクションの意味アカウント有効化メールのテンプレートビュー内では、以下のような埋め込みRubyを使うことになります。
edit_account_activation_url(@user.activation_token, ...)復習 -
edit_user_url
の挙動
edit_user_url
は、以下のような呼び出しに対し…edit_user_url(user)以下のようなURLを返します。
http://www.example.com/users/1/editこのとき、ユーザーIDの「1」は、
params[:id]
として参照することもできます。
edit_user_url
の挙動を踏まえて、edit_account_activation_url
の挙動
edit_user_url
の挙動を踏まえ、edit_account_activation_url
はどういうURLを返すのかを見てみましょう。例えば、
@user.activation_token
が"C477rjyLBojP4v5nfKcoqQ"
であるとします(@user.activation_token
はUser.new_token
の戻り値を使うのでしたね)。すると、edit_account_activation_url
は以下のURLを返す、ということになります。http://www.example.com/account_activations/C477rjyLBojP4v5nfKcoqQ/editまた、「
@user.activation_token
は、AccountActivationsコントローラーのedit
アクションにおいて、params[:id]
として参照できる」というのも重要です。アカウント有効化リンクのURLにメールアドレスも組み込む
クエリパラメータを使うことにより、アカウント有効化リンクのURLにメールアドレスも組み込みましょう。以下は、
foo@example.com
という値を、前述アカウント有効化リンクのhttp://www.example.com/account_activations/C477rjyLBojP4v5nfKcoqQ/edit?email=foo%40example.com
- クエリパラメータは、「URLの後に
?
に続けて[属性]=[値]
」という形で与えることができる
- クエリパラメータを複数与える場合は、
[属性1]=[値1]&[属性]=[値2]&...
という形で与えることができる@
をパーセントエンコーディングの%40
に変換(エスケープ)してからURL文字列を構成している
@
という文字そのものをURL中で使うことはできないため名前付きルートのオプションハッシュ
名前付きルートにオプションハッシュを与えると、当該オプションハッシュをクエリパラメータとして展開したURLを得ることができます。オプションハッシュからURLが生成される際、URLで使えない文字は自動でエスケープされ、URLセーフな文字列に変換されます。
edit_account_activation_url(@user.activation_token, email: @user.email)
@user.activation_token
の内容が"C477rjyLBojP4v5nfKcoqQ"
、@user.email
の内容が"foo@example.com"
である場合、生成されるURLは「アカウント有効化リンクのURLにメールアドレスも組み込む」項で提示したURLとなります。余談 - Railsコンソールで、任意の文字列からURLセーフな文字列を得るには
CGI.escape
というメソッドを使います。# rails console --sandbox >> CGI.escape("foo@example.com") => "foo%40example.com"以下のページの記述を参考にさせていただきました。ありがとうございます。
テンプレートビューの実際の実装
ここまでの説明を踏まえて、テンプレートビューを実際に実装してみましょう。
まずはテキスト形式のテンプレートビューです。
app/views/user_mailer/account_activation.text.erbHi <%= @user.name %>, Welcome to the Sample App! Click on the link below to activate your account: <%= edit_account_activation_url(@user.activation_token, email: @user.email) %>続いて、HTML形式のテンプレートビューです。
app/views/user_mailer/account_activation.html.erb<h1>Sample App</h1> <p>Hi <%= @user.name %>,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email) %>「HTML形式のテンプレートでは、リンクを正しく構成するために、
link_to
メソッドを使っている」というのはポイントです。演習 - 送信メールのテンプレート
1. コンソールを開き、
CGI
モジュールのescape
メソッド (リスト 11.15) でメールアドレスの文字列をエスケープできることを確認してみましょう。このメソッドで"Don't panic!"
をエスケープすると、どんな結果になりますか?
CGI.escape
については、項目「余談 - Railsコンソールで、任意の文字列からURLセーフな文字列を得るには」で先に触れましたね。>> CGI.escape("Don't panic!") => "Don%27t+panic%21"送信メールのプレビュー
Railsのメールプレビュー機能
Railsには、「特殊なURLにアクセスすることにより、メーラーにより送信されるメールのメッセージ内容をその場でプレビューすることができる」という機能があります。これを「メールプレビュー機能」と呼びます。
例えばdevelopment環境でメールプレビュー機能を利用するには、
config/environments/development.rb
の内容を以下のように変更する必要があります。config/environments/development.rbRails.application.configure do ...略 # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = true + config.action_mailer.delivery_method = :test + host = 'localhost:3000' + config.action_mailer.default_url_options = { host: host, protocol: 'https' } ...略 end
ホスト名について
development環境のメール設定において、ホスト名が必要になる場面があります。当該ホスト名は、各自のdevelopment環境に応じて変更する必要があります。
私自身の開発環境は、「Dockerで仮想環境を構築→仮想環境のディレクトリをローカルのディレクトリにマウント」という形で構築しています。ポート番号の紐付け状況は以下の通りです。
>>> docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 705320d4d96d ruby:2.5.1 "/bin/bash" 4 months ago Up 3 weeks 0.0.0.0:8080->3000/tcp rails_tutorial_test「Docker環境の3000番ポートを、ローカルの8080番ポートに紐付ける」という実装になっています。なので、ホスト名は
localhost:3000
になりますね。Userメイラーのプレビューファイルの変更
Userメイラーのプレビューファイルの場所は、
test/mailers/previews/user_mailer_preview.rb
となります。今回実装した
account_activation
メソッドが正常に動作するためには、有効なUserオブジェクトを引数として与える必要があります。そのため、UserMailerPreview#account_activation
の実装も、自動生成されたものから変更する必要があります。追加する実装は以下のとおりです。
account_activation
に渡すために、有効なUserオブジェクトの変数を定義する- 仮想属性
activation_token
に、有効なトークンを格納しておくUserMailerPreview#account_activationdef account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) endまた、私の環境は、「Docker環境の3000番ポートを、ローカルの8080番ポートに紐付ける」という実装です。ゆえに、アカウント有効化メールのプレビューをブラウザで見る際には、ポート番号は3000ではなく8080に変える必要もあります。
上記を踏まえた上で、
test/mailers/previews/user_mailer_preview.rb
は以下のように変更していきます。test/mailers/previews/user_mailer_preview.rb- # Preview all emails at http://localhost:3000/rails/mailers/user_mailer + # Preview all emails at http://localhost:8080/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/account_activation + # Preview this email at http://localhost:8080/rails/mailers/user_mailer/account_activation def account_activation + user = User.first + user.activation_token = User.new_token - UserMailer.account_activation + UserMailer.account_activation(user) end ...略 end実際にアカウント有効化メールをプレビューする
ここまでの実装が完了すれば、指定のURLにアクセスすることにより、アカウント有効化メールをプレビューすることができるようになります。
上記はHTML形式のアカウント有効化メールのプレビューです。
上記はテキスト形式のアカウント有効化メールのプレビューです。
演習 - 送信メールのプレビュー
1.Railsのプレビュー機能を使って、ブラウザから先ほどのメールを表示してみてください。「Date」の欄にはどんな内容が表示されているでしょうか?
当該プレビューページを開いた日付・時刻が表示されています。「+0000」とあるので、時刻はUTCです。
また、From:とTo:に指定されたメールアドレスの内容は以下のとおりです。
- From:には
app/mailers/application_mailer.rb
のdefault
で指定したメールアドレスが表示されている- To:にはdevelopment環境の1番目のユーザーのメールアドレスが表示されている
db/seeds.rb
で指定したとおり送信メールのテスト
自動生成されたテスト例
rails generate mailer
では、テストも自動生成されます。test/mailers/user_mailer_test.rbrequire 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do mail = UserMailer.account_activation assert_equal "Account activation", mail.subject assert_equal ["to@example.org"], mail.to assert_equal ["from@example.com"], mail.from assert_match "Hi", mail.body.encoded end test "password_reset" do mail = UserMailer.password_reset assert_equal "Password reset", mail.subject assert_equal ["to@example.org"], mail.to assert_equal ["from@example.com"], mail.from assert_match "Hi", mail.body.encoded end end
assert_match
メソッドRailsのテストで使うことができるメソッドの一つです。このメソッドを使うことにより、正規表現で文字列をテストすることができます。
assert_match 'foo', 'foobar' # true assert_match 'baz', 'foobar' # false assert_match /\w+/, 'foobar' # true assert_match /\w+/, '$#!*+@' # false
CGI.escape
メソッド「演習 - 送信メールのテンプレート」でも登場したメソッドです。今回は、テスト用のユーザーのメールアドレスをエスケープする目的で用います。
CGI.escape(user.email)実際のテストの内容
実際のテストの内容は、以下の通りになります。
test/mailers/user_mailer_test.rbrequire 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:rhakurei) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end end上記テストでは、以下の条件を満たすことをテストしています。また、「
user
の属性値たる文字列については正規表現でテストしている(assert_match
)」というのもポイントですね。
- メールの題名が「Account Activation」であること
- 宛先メールアドレスが
user
の- 送信元メールアドレスが
noreply@example.com
であること- メール本文に、有効化対象ユーザーの以下の属性値が含まれていること
- ユーザー名(
user.name
)- 有効化トークン(
user.activation_token
)- メールアドレス(
user.email
)をURLセーフになるようにエスケープした文字列現状では、設定内容に不足があるためテストは通らない
# rails test:mailers Started with run options --seed 41421 ERROR["test_account_activation", UserMailerTest, 2.0582374000223354] test_account_activation#UserMailerTest (2.06s) ActionView::Template::Error: ActionView::Template::Error: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true app/views/user_mailer/account_activation.html.erb:9:in `_app_views_user_mailer_account_activation_html_erb__2319365802641605915_47168025798700' app/mailers/user_mailer.rb:5:in `account_activation' test/mailers/user_mailer_test.rb:8:in `block in <class:UserMailerTest>' 1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.07246s 1 tests, 0 assertions, 0 failures, 1 errors, 0 skipsActionView::Template::Error: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to trueapp/views/user_mailer/account_activation.html.erb(9行目)<%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email) %>「RailsメイラーでフルURLを与える必要があるのに、ホスト名のデフォルト値が設定されていない」という趣旨のエラーメッセージです。今回はtest環境での設定の記述不足なので、
config/environments/test.rb
の内容を追加する必要があります。「RailsメイラーでフルURLを与える際に用いるホスト名のデフォルト値」は、「
config.action_mailer.default_url_options
に与えるハッシュのhost
属性の値」となります。config/environments/test.rbRails.application.configure do ...略 # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.default_url_options = { host: 'example.com' } ...略 end
Qiita内においては、@jonnyjonnyj1397さんの「Rails Missing host to link to! のエラーが出た時」という記事で、同様の状況について言及されています。
今度こそテストは成功する
# rails test:mailers Started with run options --seed 14853 1/1: [===================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.76254s 1 tests, 9 assertions, 0 failures, 0 errors, 0 skipsここまでの実装が完了し、
config/environments/test.rb
のconfig.action_mailer.default_url_options
の設定が完了しているなら、(他にtypo等がなければ)現時点でtest/mailers/user_mailer_test.rb
テストは成功するはずです。演習 - 送信メールのテスト
1.この時点で、テストスイートが
green
になっていることを確認してみましょう。# rails test Running via Spring preloader in process 13661 Started with run options --seed 63299 44/44: [=================================] 100% Time: 00:00:07, Time: 00:00:07 Finished in 7.88171s 44 tests, 192 assertions, 0 failures, 0 errors, 0 skips現時点における全体のテストの結果は以下のようになります。テスト44個・アサーション192個とは、今回Railsチュートリアルで開発中のサンプルアプリケーションも随分と成長したものです。
2. リスト 11.20で使った
CGI.escape
の部分を削除すると、テストがred
に変わることを確認してみましょう。
test/mailers/user_mailer_test.rb
の内容を以下のように変更したとしたらどうなるでしょうか。test/mailers/user_mailer_test.rbrequire 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:rhakurei) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded - assert_match CGI.escape(user.email), mail.body.encoded + assert_match user.email, mail.body.encoded end endメイラーに対するテストを実行してみます。
# rails test:mailers Started with run options --seed 22273 FAIL["test_account_activation", UserMailerTest, 1.0135770000051707] test_account_activation#UserMailerTest (1.01s) Expected /rhakurei@example\.com/ to match # encoding: US-ASCII "...略http://example.com/account_activations/4e6BdYwK0QsIciOrjytusQ/edit?email=rhakurei%40example.com\...略<a href=\"http://example.com/account_activations/4e6BdYwK0QsIciOrjytusQ/edit?email=rhakurei%40example.com\">Activate</a>...略". test/mailers/user_mailer_test.rb:13:in `block in <class:UserMailerTest>' 1/1: [===================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.01736s「メール本文中に
/rhakurei@example\.com/
という正規表現が見つからない」という趣旨のメッセージを出してテストが失敗していますね。演習終了後は、
test/mailers/user_mailer_test.rb
の内容は元に戻しておくことを忘れずに。test/mailers/user_mailer_test.rbrequire 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:rhakurei) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded - assert_match user.email, mail.body.encoded + assert_match CGI.escape(user.email), mail.body.encoded end endユーザーの
create
アクションを更新「アプリケーションでメイラーを使い、新規登録ユーザーに対してアカウント有効化のメールを送信する」…以上のユースケースを実現するためには、Usersコントローラーの
create
アクションにも手を加える必要があります。実際に
app/controllers/users_controller.rb
に変更を加えるapp/controllers/users_controller.rbclass UsersController < ApplicationController ...略 def create @user = User.new(user_params) if @user.save - log_in @user - flash[:success] = "Welcome to the Sample App!" - redirect_to @user + UserMailer.account_activation(@user).deliver_now + flash[:info] = "Please check your email to activate your account." + redirect_to root_url else render 'new' end end ...略 end上記変更のポイントは以下です。
- 新規作成時点では、ユーザーはログインしないようになった
- リダイレクト先をプロフィールページからルートURLに変更
- プロフィールページへのリダイレクトは、アカウント有効化を実装するにあたっては無意味な動作であるため
現状では失敗するテストを一時的にコメントアウトする
上記変更により、現状では、
test/integration/users_signup_test.rb
内のテスト「valid signup information」が成功しない状態になっています。そのため、現状では失敗する以下のテストを一時的にコメントアウトします。
users/show
テンプレートが描画されること
- リダイレクト先をプロフィールページからルートURLに変更したため
is_logged_in?
が真であること
- 新規作成直後にユーザーがログインしない実装にしたため
test/integration/users_signup_test.rbrequire 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest ...略 test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password"} } end follow_redirect! - assert_template 'users/show' + # assert_template 'users/show' assert_not flash.empty? - assert is_logged_in? + # assert is_logged_in? end enddevelopment環境で、この時点で新規ユーザー作成操作を行うと?
きちんとフラッシュメッセージが表示されています。
現状のdevelopment環境においては、実際にメールが送信されることはありません。ただ、生成されたメールの内容はサーバーログに出力されます。以下はサーバーログに出力されたメール内容の例です。
Sent mail to foobar.foobar@example.com (29.5ms) Date: Thu, 05 Dec 2019 03:36:46 +0000 From: noreply@example.com To: foobar.foobar@example.com Message-ID: <5de87b4ef1756_34d82af2160b7664722c6@705320d4d96d.mail> Subject: Account activation Mime-Version: 1.0 Content-Type: multipart/alternative; boundary="--==_mimepart_5de87b4eecb86_34d82af2160b766472131"; charset=UTF-8 Content-Transfer-Encoding: 7bit ----==_mimepart_5de87b4eecb86_34d82af2160b766472131 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Hi Foo Bar Foobar, Welcome to the Sample App! Click on the link below to activate your account: https://localhost:3000/account_activations/RqhnVNn2LxnIwwhzZNUaQg/edit?email=foobar.foobar%40example.com ----==_mimepart_5de87b4eecb86_34d82af2160b766472131 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 7bit <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <style> /* Email styles need to be inline */ </style> </head> <body> <h1>Sample App</h1> <p>Hi Foo Bar Foobar,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <a href="https://localhost:3000/account_activations/RqhnVNn2LxnIwwhzZNUaQg/edit?email=foobar.foobar%40example.com">Activate</a> </body> </html> ----==_mimepart_5de87b4eecb86_34d82af2160b766472131--演習 - ユーザーの
create
アクションを更新1. 新しいユーザーを登録したとき、リダイレクト先が適切なURLに変わったことを確認してみましょう。その後、Railsサーバーのログから送信メールの内容を確認してみてください。有効化トークンの値はどうなっていますか?
前述「development環境で、この時点で新規ユーザー作成操作を行うと?」における例では、有効化トークンの内容は以下のようになっています。
RqhnVNn2LxnIwwhzZNUaQg2. コンソールを開き、データベース上にユーザーが作成されたことを確認してみましょう。また、このユーザーはデータベース上にはいますが、有効化のステータスが
false
のままになっていることを確認してください。# rails console --sandbox >> user = User.last => #<User id: 101, ...略, activation_digest: "$2a$10$d8ag86CApZUTMvuuBhlW9uEGbnbYKaKmNV4ynhLEGb8...", activated: false, activated_at: nil> >> user.activated? => falseid=101のユーザーは、「development環境で、この時点で新規ユーザー作成操作を行うと?」で新規作成したユーザーです。確かに
user.activated?
の戻り値はfalse
です。また、activated_at
の値はnil
です。
- 投稿日:2019-12-05T17:41:47+09:00
OmniAuth【google-oauth2】のログイン時のエラーJWT::InvalidIatErrorについて
状況
Railsでomniauth-google-oauth2を利用してログイン認証を行っていた際に、以下エラー「JWT::InvalidIatError」が発生した。
その解決方法の備忘録になります。解決策
結論
config/initializers/devise.rbに以下を追記する。
(Deviseを実装していた)変更前config.omniauth :google_oauth2, ENV['GOOGLE_APP_ID'], ENV['GOOGLE_APP_SECRET'], name: :google変更後config.omniauth :google_oauth2, ENV['GOOGLE_APP_ID'], ENV['GOOGLE_APP_SECRET'], name: :google, skip_jwt: true原因
セッショントークン生成時にOSの時間のずれがあり、JWT(ジョット)のデコーディング時にエラーが発生している。
omniauth-google-oauth2の公式ドキュメントにも以下の記述がありました。skip_jwt: Skip JWT processing. This is for users who are >seeing JWT decoding errors with the iat field. Always try >adjusting the leeway before disabling JWT processing.
原因確認中に遠回りしたこと
まず、エラーの原因はOSの時間のずれであり、時間設定を変更すれば解決するという情報を発見したため、時間設定を変更しようと考えたが、それでは解決できませんでした。
Railsではシステムや環境変数で設定されている日時を
Time.now
で取得ができます。
それとは別にapplication.rbにデフォルトのタイムゾーンを設定でき、
その日時はTime.zone.now
で取得ができます。現在日時を取得してみると、以下の通り
irb(main):001:0> Time.now => 2019-12-05 17:16:52 +0900 irb(main):002:0> Time.zone.now => Thu, 05 Dec 2019 08:16:57 UTC +00:00 irb(main):003:0>
Time.zone.now
ではUTC(協定世界時:Coordinated universal time)になっているということは、RailsではデフォルトではUTC設定になっています。
こちらを日本時間(JST)に変更することにしました。config/application.rbconfig.time_zone = 'Tokyo' config.active_record.default_timezone = :local上記コードを追加し、JSTにて日時取得できるようになりました。
irb(main):001:0> Time.now => 2019-12-05 17:31:29 +0900 irb(main):002:0> Time.zone.now => Thu, 05 Dec 2019 17:31:33 JST +09:00 irb(main):003:0>しかし、エラーは解決されませんでした。今回のエラーでは関係がない模様。。。
◆参考URL
Qiita、githubのissueを参考にしました。https://github.com/zquestz/omniauth-google-oauth2/issues/195
https://qiita.com/sutoh/items/b7d23990abb9c5083daa
https://qiita.com/takuchan9104/items/1b588e125f8a1e0c7605
- 投稿日:2019-12-05T17:40:14+09:00
RailsのGeocoderとあそぼ
はじめに
はじめまして、DMM WEBCAMPでメンターをしております。
昨年「Rails5でGoogleMapを表示してみるまで」という記事を書きました。
それからというもの、私は地図おじさんとなり、数多の地図表示を手助けするはめになりました。
ときには、地図に関連してGeocodingの質問もいただくようになりました。
しかしながら、Geocodingはあまり触っていなかったので、この際まとめておこうと思った次第です。Geocoding
GeoCoding(ジオコーディング)とは
Wikipediaより
ジオコーディングは、狭義には地名、住所が示す場所に対して、地理座標を与えることを言う。その場所は地点(例:市役所)では一点に定まるが、ある範囲(例:市町村)では、代表点を一点で示す場合と、領域を多角形、すなわちポリゴンで示す場合がある。代表点は応用目的により、「東西の中心、南北の中心」のように幾何的に与えるか、何らかの意味をもった地点を「市の場合は市役所の位置、川の場合は河口ないし合流点の位置」のように与える。
ジオコーディングは、また各種データ(例:地名を含む文書、ある地点を撮影した写真)に地理座標を与えることもいう。要は、住所や地名から座標(経度緯度)を取得したり、その逆を行ったりです。
これによって、近くのお店を検索したり、2地点間の真ん中にあるスポットを検索したりが可能になります。GemのGeoCoder
今回はRails環境で、GemのGeocoderを使用してGeocodingを行います。
https://github.com/alexreisner/geocoder
基本的な使い方と要所を示していきます。基本
Geocoderの基本例は次のとおりです。
地名や住所を引数にして、経度緯度を返り値として取得できます。results = Geocoder.search("Paris") results.first.coordinates #=> [48.856614, 2.3522219] latitude and longitude反対に、経度緯度を引数にして、住所などを取得できます。
results = Geocoder.search([48.856614, 2.3522219]) results.first.address #=> "Hôtel de Ville, 75004 Paris, France"一方で、ipアドレスから場所を取得することも可能です。
results = Geocoder.search("172.56.21.89") results.first.coordinates #=> [30.267153, -97.7430608] results.first.country #=> "United States"モデルに対するGeocoding
ここからはActiveRecordを使用したモデルに対してGeocoderを利用する方法を紹介していきます。
この際、大事なポイントが3つあります。
1. 地名・住所データを返すメソッドが存在している。
2. 経度・緯度データを格納するカラムが存在している。
3.geocoded_by
もしくはreverse_geocoded_by
をモデルに記述している。1. 地名・住所データを返すメソッドが存在している
地名・住所データを返すメソッドがあればよいので、データベースのカラムとして持つか、モデル内にメソッドを用意しましょう。
サンプルコードでは、データベースのカラムとしてaddressを用意しています。
モデル内にメソッドを設ける場合は、特定の場所や、複数カラム組み合わせて返すなど工夫を凝らすことが容易になります。class Model < ApplicationRecord def current_position #現在地を返す end def address [street, city, state, country].compact.join(', ') end end2. 経度・緯度データを格納するカラムが存在している。
デフォルトの設定だと、
latitude
とlongitude
というカラムを用意する必要があります。
これは、モデルで設定を変更できます。class Model < ApplicationRecord #lat, lonというカラムを設ける場合 geocoded_by :address, latitude: :lat, longitude: :lon # ActiveRecord end3.
geocoded_by
もしくはreverse_geocoded_by
をモデルに記述している。すでに
2. 経度・緯度データを格納するカラムが存在している。
で記述してしまいましたが、geocoded_by
かreverse_geocoded_by
を記述することで、モデルを介してgeocoderのメソッドを使えるようになります。
reverse_geocoded_by
は経度緯度でgeocodingしたい場合に記述します。obj.distance_to([43.9,-98.6]) # distance from obj to point obj.bearing_to([43.9,-98.6]) # bearing from obj to point obj.bearing_from(obj2) # bearing from obj2 to obj
ActiveRecordのモデルに対してGeocoderを備え付ける方法はこれまでのとおりです。
ここからは例を示しながらGeocoderで使えるメソッドを紹介していきます。
ここではSpotモデルを作成し、そのモデルで遊んでいきます。サンプルコードでは適当なデータをseed.rbに入れてあります。
ジオコーディング
geocode
メソッドでgeocodingできます。
after_validation
のコールバックとしてgeocodeを行う例をよく見ます。class Spot < ApplicatoinRecord geocoded_by :address after_validation :geocode endSpot.create(:address => "東京タワー") => #<Spot id: 1, address: "東京タワー", latitude: 35.65858645, longitude: 139.745440057962, created_at: "2019-12-05 06:06:19", updated_at: "2019-12-05 06:06:19">距離算出
距離算出には、
distance_to
、distance_from
が使えます。
ただし、geocoderの距離単位はデフォルトでmileになっています。spot1 = Spot.find(1) spot2 = Spot.find(2) spot1.distance_to(spot2) spot1.distance_from([35.65858645, 139.745440057962])近いやつ取得
多くの初心者エンジニアが使ってみたい機能ですね。
geocoderだとめちゃめちゃかんたんです。Spot.near("新宿") #20mileで検索 spot = Spot.find(1) spot.nearbys(5, units: :km) #5kmで検索検索SQL見ると楽しいです。筋肉で押し通す感じです。
こんなものなんでしょうか。SELECT spots.*, (69.09332411348201 * ABS(spots.latitude - 35.6891183) * 0.7071067811865475) + (59.836573914187355 * ABS(spots.longitude - 139.7010768) * 0.7071067811865475) AS distance, CASE WHEN (spots.latitude >= 35.6891183 AND spots.longitude >= 139.7010768) THEN 45.0 WHEN (spots.latitude < 35.6891183 AND spots.longitude >= 139.7010768) THEN 135.0 WHEN (spots.latitude < 35.6891183 AND spots.longitude < 139.7010768) THEN 225.0 WHEN (spots.latitude >= 35.6891183 AND spots.longitude < 139.7010768) THEN 315.0 END AS bearing FROM "spots" WHERE (spots.latitude BETWEEN 35.3996547337783 AND 35.978581866221695 AND spots.longitude BETWEEN 139.34467987351536 AND 140.05747372648466) ORDER BY distance ASC LIMIT ?中間地点を取得
これぐらい自分で計算できますが、メソッドを用意してくれてます。
マッチングアプリでお互いの中間地点にあるスポット検索とかできたら便利ですね。spot1 = Spot.find(1) spot2 = Spot.find(2) Geocoder::Calculations.geographic_center([spot1, spot2]) #引数は3箇所以上可能 => #[35.68432475413425, 139.77806655172708]Geocodingの設定
距離の単位をkmにしたり、Geocodingを外部APIにするなどの設定を行うことができます。
rails g geocoder:config
で設定ファイルを生成できます。geocoder.rbGeocoder.configure( units: :km )googleのgeocoding APIを使用する場合は次のように設定します。
geocoder.rbGeocoder.configure( lookup: :google, api_key: '---------YOUR_API_KEY---------', )GoogleのGeocodingAPIを使わずとも、geocoderの標準である程度の精度は出ます。
ただ、住所検索のときにはGoogleのGeocodingAPIを通したほうがいいかなとは思います。その他のGeocoding APIを使用する場合はAPI_GUIDEを参照してください。
おわりに
Geocoderを使うとGeocodingがとても楽ですね。
これで僕は地図おじさんから地理おじさんに昇格です。
(ニッチすぎたと反省しております。)
- 投稿日:2019-12-05T16:41:14+09:00
フィクスチャとFactoryBotの混在
問題
- 7年前からのアプリケーション。フィクスチャとFactoryBotを使ったspecが混在している。フィクスチャを使ったspecを書き直すのは面倒。
- feature specをsystem specに変更し、DatabaseCleanerを使うのをやめ、use_transactional_fixturesをtrueに変更した。
- フィクスチャデータがテストDBに残って、FactoryBotを使ったspecに影響を与えるので困る。
フィクスチャとRspecのHooks
「use_transactional_fixtures = true」では、次のような流れになります。投入されたフィクスチャデータは消されません。フィクスチャデータが一度投入されると、テーブル名がキャッシュに保存され、次に「fixtures テーブル名」が来たら投入はスキップ、となります。
before(:context) DELETE FROM フィクスチャのテーブル INSERT INTO フィクスチャのテーブル BEGIN before(:example) テストコード実行 after(:example) ROLLBACK BEGIN before(:example) テストコード実行 after(:example) ROLLBACK after(:context)対策
after(:context)でレコードを消し、フィクスチャのキャッシュも消す。
config.after(:context) do |example| fixtures = ActiveRecord::FixtureSet.cached_fixtures(ActiveRecord::Base.connection) fixtures.collect(&:model_class).each(&:delete_all) ActiveRecord::FixtureSet.reset_cache end上記の変数fixturesは、ActiveRecord::FixtureSetのインスタンスの配列です。Railsのソースはこちら。
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/fixtures.rb
- 投稿日:2019-12-05T15:36:47+09:00
rails cが動かねぇ!
rails c が息をしていない…
別件でrailsのバージョンアップをして、railsを入れ直した後のこと。
consoleで確認したいことがあり、rails c
を叩く。
…反応がない、ただの屍のようだ。
いや困るねん。やめてクレメンス。いらないプロセスは躊躇なく殺そうね
てことでプロセスを確認。
ps aux | grep rails…何もないな。
調べたところ、rails c
ではspringというプロセスも走るとのこと。ps aux | grep spring ~~ 8324 0.0 0.0 4352360 3228 s009 S+ 火03PM 0:00.76 spring server | ~~ | started 47 hours agoいやがった!!お前のせいか!!
kill -9 8324そしてめでたくconsoleが起動。
いやぁ良かった。
無駄なプロセスは殺そう。追記
spring stopでも可。
これだとプロセス確認せずに止められるから便利ね。
ありがとう、友よ。
- 投稿日:2019-12-05T14:34:44+09:00
ActiveRecordを使いつつDBのデータをメモ化して扱ってみる
はじめに
一度登録されたら、その後あまり更新されないマスター系のデータってありますよね。
普通にDBのテーブルを作成してActiveRecordを使ったり、moduleやclassにデータをハードコーディングして扱う場合もあると思います。テーブルが不要な場合は ActiveHash というgemを使うと便利で高速ですが、なんらかの理由でDBを使いたい場合もあるかもしれません。
(例えば、Railsから切り離されたレポートシステムなどから同じマスターデータを参照したい場合など)
ということで、ActiveRecord を利用しつつ普段はオンメモリで高速にデータを扱う簡単なライブラリを作成してみました。実装したもの要約
- マスター系データを扱うModelクラスで、全レコードのオブジェクトをメモ化するメソッド
- メモ化したデータを対象にfind, selectを行うメソッド
- 上記Modelに対しbelongs_toの関係にあるModelから参照する場合も、メモ化されたオブジェクトを利用するAssociation
使い方
インストール
RubyGemsに登録したのでGemfileを使ったりgemコマンドを使ってインストールできます。
https://rubygems.org/gems/ar_memoizationGemfilegem 'ar_memoization'DBスキーマの例
サンプルとして、以下のように都道府県を扱う
prefectures
と、お店情報を扱うshops
テーブルを定義しました。
shops
はprefectures
に対する外部キーを持っており、prefectures
はあまり更新されないものとします。db/create_tables.rbclass CreateAllTables < ActiveRecord::Migration[5.0] def self.up create_table(:prefectures) do |t| t.string :name end create_table(:shops) do |t| t.belongs_to :prefecture t.string :name end end endマスターデータを定義するModelクラス
全レコードのインスタンスをメモ化したいModelクラス内で
ArMemoization::PrimaryMethods
モジュールをextendします。app/models/prefecture.rb# # 都道府県を扱うModel # class Prefecture < ApplicationRecord extend ArMemoization::PrimaryMethods end追加されるメソッド
- .find_memo(id)
- メモ化したインスタンスから、引数のIDに一致するデータを返す
- IDをキーにしたHashから該当レコードのオブジェクトを返すので爆速
- .detect_memo(&block)
- メモ化したレコードのオブジェクトをブロック引数で渡し、ブロックがtrueを返したデータを1件返す
- select_memos(&block)
- メモ化したレコードのオブジェクトをブロック引数で渡し、ブロックがtrueを返したデータを配列で返す
- all_memos
- メモ化したインスタンスの配列を全て返す
- reload_memos
- メモ化したオブジェクトをDBからリロードする
使い方の例
事前準備(データ投入)[[1, "東京"], [2, "大阪"], [3, "名古屋"]].each do |ident, name| Prefecture.create!(id: ident, name: name) end# ID:2 のレコードを取得 prefecture = Prefecture.find_memo(2) # インスタンスメソッド kanto_area? がtrueを返すオブジェクトを取得 prefecture = Prefecture.detect_memo{|pref| pref.kanto_area? } prefecture = Prefecture.detect_memo(&:kanto_area?) # syntax sugar # インスタンスメソッド kanto_area? がtrueを返すオブジェクトの配列を取得 prefectures = Prefecture.select_memos(&:kanto_area?)実装の詳細はこちら
マスターデータへの外部キーを持つModelクラス
マスターデータのModelに対してbelongs_toの関係を持つModelクラスには
ArMemoization::ForeignMethods
をextendします。
また、belongs_to
の代わりにbelongs_to_memoized
を使いAssociationを定義します。app/models/shop.rbclass Shop < ApplicationRecord extend ArMemoization::ForeignMethods belongs_to_memoized :prefecture end(通常ないと思いますが)belongs_toと同時に利用することも出来ます。
app/models/shop.rbbelongs_to :prefecture belongs_to_memoized :memoized_prefecture, class_name: "Prefecture", foreign_key: "prefecture_id"
belongs_to_memoized
は内部でbelongs_to
を実行した後で、関連名でもあるreaderメソッド(上記例ではShop#prefecture
)をoverrideし、レコードをDBからロードする処理の代わりにメモ化済みのオブジェクトをAssociationとしてセットしています。Association以外で追加されるメソッド
- .where_memoized(association_name, method_name, &block)
- 関連Modelのメモ化されたオブジェクトを用い、join + where 的な絞り込みを行う
- ActiveRecord::Relation を返す
使い方の例
# Prefectureオブジェクトの中で、kanto_area?がtrueを返すオブジェクトのIDに関連しているShopのRelationを返す # 第2引数として関連Modelのインスタンスメソッド名か、ブロックを渡す Shop.where_memoized(:prefecture, :kanto_area?).limit(3) Shop.where_memoized(:prefecture){|pref| pref.kanto_area? }.limit(3)発行されるクエリSELECT `shops`.* FROM `shops` WHERE `shops`.`prefecture_id` = 1 LIMIT 3同一の条件文を実装するのに、scopeとインスタンスメソッドの両方でコードを書く必要がなくなるので、自分的にはちょっと良い感じです。
実装の詳細はこちら
今は出来ないけど出来そうなこと
- ID以外のPrimary Key に対応
- has_one, has_many からメモ化済みオブジェクトの取得
- create, saveのコールバックでメモ化したオブジェクトを差し替える
- 開発環境ではメモ化したオブジェクトを適切なタイミング(before_actionなど)で簡単にリロードする仕組み
まとめ
ほとんどの場合はActiveHashを使えば良いと思うので、このライブラリの使いどころがあまり思いつきません。とりあえず「こんなのあったらどうだろう?」な思いつきを形にしてみました。むしろ使い道を教えてください?
それでは、もっと有意義な時間を過ごすべくデザインパターンの勉強でもしてみましょう。
12日目は @nagata03 さんです!
- 投稿日:2019-12-05T14:11:16+09:00
普段使っているgemにプルリクを送ってマージされた
(この記事は、高専OBOG Advent Calendar 2019の12/5に送った記事です。)
要約
普段使っているgemに、Pull Requestを送って、マージされました。
LazyHighChartsというgemの動きが気に入らなくてモンキーパッチを当てて使っていました。
自分のアプリケーション以外でも役に立つ、gem本体に取り込まれるべき修正だと考えたので、gemのリポジトリにプルリクを送ることにしました。流れはこんな感じです。
- 不具合を見つける
- 手元でgemの修正を試す
- githubのリポジトリに修正を送る
- マージされる
PRがマージされるまで
1. 不具合を見つける
LazyHighChartsは、Highcharts JSというチャートを描画するJavaScriptライブラリを、RailsやSinatraなどのフレームワーク内で簡単に使えるようにするgemです。
詳しい紹介は本家をご覧ください。https://github.com/michelson/lazy_high_chartsRailsでJavaScriptの読み込みにdefer属性を付与した場合、チャートが描画されない、という不具合に遭遇しました。
viewに書き出されたjsを見てみると、どうやらLazyHighChartsがこしらえたjsに問題があるようでした。2. 手元でgemの修正を試す
手元でgemの修正をためします。
問題になっている箇所を書き出しているModuleを探して、モンキーパッチを当てます。
IntelliJ IDEAだとコードジャンプで該当箇所に飛べるので、すぐにModuleやClassの名前がわかって便利ですね!
モンキーパッチを当てると、問題なくチャートが描画されました。
(しばらくこの状態で本番で使っていました)次はgemのリポジトリをforkして、gemに修正を加えます。
Gemfileのlazy_high_chartsの取得先をこんな感じにforkしたリポジトリ&ブランチに書き換えて、手元で修正版のgemを試します。gem 'lazy_high_charts', git: 'git@github.com:kosappi/lazy_high_charts.git', branch: 'use_event_listener'この修正版も、モンキーパッチのときと同じように、問題なくチャートを描画してくれました。
3. githubのリポジトリに修正を送る
手元で動作確認ができたので、本家にも同じ修正を送ります。
まずはREADME.mdを読んで、どのような流れで修正を提案するのか見てみます。README.md
Contributingを読んでみると、このように書いてあります。
- Fork the project
- Do your changes and commit them to your repository
- Test your changes. We won't accept any untested contributions (except if they're not testable).
- Create an issue with a link to your commits.
まずは動作確認して、それからissueを作って欲しい、とのことでした。
動作確認は済んでいるので、issueを作ります。
英語は得意ではないので、すでにマージされたPRや、closeされたissueを参考にしつつ、時間をかけて書くことになりました。
作ったissue1日経つと、メンテナからPRを作って欲しいという旨の返事がありました。
PRを作ります。
作ったPRPRを作ってしばらくするとCIがコケて赤くなっていました。
specを少し修正するだけで解決したんですが、事前にforkしたリポジトリでもCIを回しておくと、ベターかも知れません。あとはマージされるのを待ちます。
4. マージされる
PRを出してから1日後、マージされました。
やったね!感想
RubyやRailsを仕事で触りだして数年になりますが、めちゃくちゃgemを使うわりには、contributeする機会はまったくありませんでした。
タダで使ってばっかりでなんかスマン、という気持ちがありました。
今回、はじめてcontributeすることができて、とてもうれしいです。
- 投稿日:2019-12-05T14:11:16+09:00
LazyHighChartsにプルリクを送ってマージされた
(この記事は、高専OBOG Advent Calendar 2019の12/5に送った記事です。)
要約
普段使っているgemに、Pull Requestを送って、マージされました。
LazyHighChartsというgemの動きが気に入らなくてモンキーパッチを当てて使っていました。
自分のアプリケーション以外でも役に立つ、gem本体に取り込まれるべき修正だと考えたので、gemのリポジトリにプルリクを送ることにしました。流れはこんな感じです。
- 不具合を見つける
- 手元でgemの修正を試す
- githubのリポジトリに修正を送る
- マージされる
PRがマージされるまで
1. 不具合を見つける
LazyHighChartsは、Highcharts JSというチャートを描画するJavaScriptライブラリを、RailsやSinatraなどのフレームワーク内で簡単に使えるようにするgemです。
詳しい紹介は本家をご覧ください。https://github.com/michelson/lazy_high_chartsRailsでJavaScriptの読み込みにdefer属性を付与した場合、チャートが描画されない、という不具合に遭遇しました。
viewに書き出されたjsを見てみると、どうやらLazyHighChartsがこしらえたjsに問題があるようでした。2. 手元でgemの修正を試す
手元でgemの修正をためします。
問題になっている箇所を書き出しているModuleを探して、モンキーパッチを当てます。
IntelliJ IDEAだとコードジャンプで該当箇所に飛べるので、すぐにModuleやClassの名前がわかって便利ですね!
モンキーパッチを当てると、問題なくチャートが描画されました。
(しばらくこの状態で本番で使っていました)次はgemのリポジトリをforkして、gemに修正を加えます。
Gemfileのlazy_high_chartsの取得先をこんな感じにforkしたリポジトリ&ブランチに書き換えて、手元で修正版のgemを試します。gem 'lazy_high_charts', git: 'git@github.com:kosappi/lazy_high_charts.git', branch: 'use_event_listener'この修正版も、モンキーパッチのときと同じように、問題なくチャートを描画してくれました。
3. githubのリポジトリに修正を送る
手元で動作確認ができたので、本家にも同じ修正を送ります。
まずはREADME.mdを読んで、どのような流れで修正を提案するのか見てみます。README.md
Contributingを読んでみると、このように書いてあります。
- Fork the project
- Do your changes and commit them to your repository
- Test your changes. We won't accept any untested contributions (except if they're not testable).
- Create an issue with a link to your commits.
まずは動作確認して、それからissueを作って欲しい、とのことでした。
動作確認は済んでいるので、issueを作ります。
英語は得意ではないので、すでにマージされたPRや、closeされたissueを参考にしつつ、時間をかけて書くことになりました。
作ったissue1日経つと、メンテナからPRを作って欲しいという旨の返事がありました。
PRを作ります。
作ったPRPRを作ってしばらくするとCIがコケて赤くなっていました。
specを少し修正するだけで解決したんですが、事前にforkしたリポジトリでもCIを回しておくと、ベターかも知れません。あとはマージされるのを待ちます。
4. マージされる
PRを出してから1日後、マージされました。
やったね!感想
RubyやRailsを仕事で触りだして数年になりますが、めちゃくちゃgemを使うわりには、contributeする機会はまったくありませんでした。
タダで使ってばっかりでなんかスマン、という気持ちがありました。
今回、はじめてcontributeすることができて、とてもうれしいです。
- 投稿日:2019-12-05T12:41:42+09:00
Rails6 のちょい足しな新機能を試す110(TranslationHelper#translate default オプション編)
はじめに
Rails 6 に追加された新機能を試す第110段。 今回は、
TranslationHelper#translate
default
オプション編です。
Rails 6 では、TranslationHelper#translate
のdefault
オプションで指定された値が Hash の場合に、その Hash が返されるようになりました。Ruby 2.6.5, Rails 6.0.1 で確認しました。(Rails 6.0.0 時点で修正されています。)
$ rails --version Rails 6.0.1今回は、適切な使い道を思いつけませんでした。
で、言語 (locale) 毎に数字をカンマ区切りで表示するだけの、Railsアプリケーションを作って試してみることにします。Rails プロジェクトを作成する
$ rails new rails_sandbox $ cd rails_sandboxController と View を作る
今回は、数字を表示するための NumbersController と index ページを作成します。
$ bin/rails g controller Numbers index
Helper メソッドを作成する
locale_number_with_delimiter
メソッドを作成します。
引数は、 数字と locale です。
default_number_format メソッドをコールして、locale に対するデフォルトフォーマットを決定します。
TranslationHelper#translate
メソッド (t
メソッド)を使って、number.format
を取得し、取得できなかった場合のために、default を指定します。
ここが、今回の確認ポイントになります。
number_with_delimiter
メソッドで数字を変換します。app/helpers/numbers_helper.rbmodule NumbersHelper def locale_number_with_delimiter(number, locale) default = default_number_format(locale) format = t('number.format', locale: locale, default: default) # ここがちょい足し機能 number_with_delimiter(number, format) end private def default_number_format(locale) case locale when :en, :ja { delimiter: ',', separator: '.' } when :de, :it { delimiter: '.', separator: ',' } when :sv { delimiter: ' ', separator: ',' } when :ruby { delimiter: '_', separator: '.' } else { delimiter: ',', separator: '.' } end end endController を修正する
NumbersController
のindex
メソッドで、@locales
と@number
を設定します。app/controllers/numbers_controller.rbclass NumbersController < ApplicationController def index @locales = %i[en ja de it sv ruby] @number = 10000.5 end endView を修正する
View では、 Language(Locale) と数字を表形式で表示します。
app/views/numbers/index.html.erb<h1>Numbers#index</h1> <table> <thead> <tr> <th> Language </th> <th> Number </th> </tr> </thead> <tbody> <% @locales.each do |locale| %> <tr> <td><%= locale %></td> <td><%= locale_number_with_delimiter(@number, locale) %></td> </tr> <% end %> </tbody> </table>config/application.rb を修正する
動作確認目的なので、locale のエラーが出ないように、
config/application.rb
に、I18n.enforce_available_locales = false
を追加します。module App class Application < Rails::Application ... I18n.enforce_available_locales = false end endrails server を実行してブラウザで表示する
rails server
を実行してブラウザで表示すると、それぞれの Language(locale) に合わせた数字の表示になっています。
Rails 5では
TranslationHelper#translate
が default で指定した Hash を返さないため、NoMethodError になってしまい動作しません。
試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try110_translate_default参考情報
- 投稿日:2019-12-05T11:09:27+09:00
RailsとSwiftで動画アップローダーを作る?
まえがき
知らぬ間にまたアドベントカレンダーの季節ですね?
今年もノリと勢いが前のめりすぎて立候補しましたがギリギリです。
(いつになったら一日は30時間になるのか。。)
(ちなみに去年こんなの書きました✌?10分クッキング!誰でもインフルエンサーになれるinstabotの作り方?)今年のテーマは、動画アップロード機能です。
画像は馴染みがあるけど動画はやったことないなんて人、多くないでしょうか?
そんな私を含めた方達に向けて、ゆるく解説していこうと思います◎?こんなの作ります
機能は、動画選択・プレビュー・アップロード・最新の動画をフェッチして再生です。たぶんレアな記事なので是非お付き合いください?
(あといいね欲しいですね。。!もう押してもらって大丈夫です???)お勧めしたい人
- Ruby(Rails)やったことあるよ!
- HTTPリクエストなんとなくわかるよ!
- RailsのAPIモードちょっと触ってみたいよ!
- モバイルアプリやってみたいよ!
- Swiftやってみたいよ!
- Swift初心者だよ!
- 動画アップロードやってみたいよ!
- というか開いてくれた人 みんなですね!
(せっかく一年に一回のアドベントカレンダーってお祭りなので、みんな読んでね!おねがいします!)
ようこそ〜〜〜?
使用技術
サーバーサイド(API)にRuby
フロントエンド(モバイル)にSwiftの構成です。Ruby 2.6.2
Ruby on Rails 5.2.3
Swift 5.0.1
Xcode 10.2.1
(ちょっと古いので上げます?)まだまだやれるぜって人へ
実際にアプリをストアにあげるのに、Railsプロジェクトのデプロイは必須なのでHerikuデプロイとか挑戦してみてください?
【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】いざ実装??
全体のザックリした流れ
- Swiftで動画アップローダーを作る
- SwiftでAPIクライアントを作る
- RailsでAPIサーバーを作る
- Swiftで動画プレイヤーを作る
~ 完成 ~
1. [Swift] 動画アップローダーの実装?
ザックリした流れ
- プロジェクトの作成
- シュミレーターにサンプル動画を追加
- 今回使用するファイルの説明
- 動画選択の機能を作成する
- 動画再生の機能を作成する
- 動画アップロードの機能を作成する
プロジェクトの作成
- Xcodeを起動する
- Create a new Xcode projectを選択する
- Single View Appを選択してNextを選択する
- Product Name(好きなアプリ名)を入力してNextを選択する
- 保存先を指定してCreateを選択する
これでSwiftプロジェクトの作成ができました!
シュミレーターにサンプル動画を追加
シュミレーターとは、書いたプログラムの動作を、mac内で確認するためのものです。
(Railsで動作確認する時、ブラウザにlocalhost:3000と入力して見る画面のようなものです。)シュミレーターには元々サンプル画像しか入っていないので、ここで動画を追加します。
まだ何もコードは書いていませんが起動(ビルド)してみましょう!
- 起動したいデバイス(自分はiPhoneXR)を指定して左上の再生ボタンを押します
![]()
- シュミレーターを起動したら真っ白な画面が出ますがメニューに戻り、写真アプリを開きます
- macのfinderから好きな動画を選択します(なければスマホなどからmacに送ってください)
- シュミレーターにドラッグアンドドロップで追加します
今回使用するファイルの説明
ここから実際に画面を作っていきますが、
先に今回使用するファイルについてザックリ説明しておきます。
Main.storyboard(自動生成・自作も可)
storyboardはUI部品(ボタンや画像表示するのに必要なパーツなど)を置いて、
直感的にレイアウトを作成するファイルです。
細かい機能やレイアウトを実装はこのファイルでは実装しきれません。あくまでレイアウトをザックリ構築するファイルです。ViewController.swift(自動生成・自作も可)
storyboardでカバーできない機能的な実装や細かいレイアウトを記述するファイルです。
例えば、ボタンが押された時の挙動やAPIから受け取ったデータをUIに受け渡す処理なんかを書きます。今回はVideoUploaderViewController.swiftを作成します。
APIClient.swift(自作)
バックエンド(APIサーバ)へのリクエストやレスポンスを受け取るのに使用します。Info.plist(自動生成・自作も可)
設定ファイルです。今回はここでフォトライブラリへのアクセス許可の設定をします。動画選択の機能を作成する
ここでやること
1. VideoUploaderViewController.swiftを作成する
2. Main.storyboardに動画選択用のボタンを追加する
3. ユーザーにカメラロールの使用許可をとる実装を追加する
4. ボタンをViewControllerに接続する
5. ボタンが押されたときにフォトライブラリを開いて動画を選択する
6. 選択された動画のサムネイルをViewControllerに表示する1. VideoUploaderViewController.swiftを作成する
基本的に、1ページを作るのにUI部品を配置するStoryboard(ViewController)と機能を実装するViewControllerがセットで必要です。
はじめに動画の選択からアップロードまでを行うページを作っていきます。
このページは
storyboardに自動生成されるMain.storyboard(ViewController)と、
ViewControllerは自動生成されるViewController.swiftをリネームして作成していきます。storyboardは自動生成なので、まずはViewController.swiftをリネームていきます。
Xcodeの左側にあるナビゲーターからプロジェクターナビゲーター
(左上のファイルアイコンを押すと出てきます。ファイルの一覧です)を選択し、
ViewControllerをクリックして名前をVideoUploaderViewController.swiftに変更してください。
次に、セットで使用するstoryboardに紐づいているViewController名を変更します。
左側のナビゲーターからMain.storyboardを選択します。ここにはViewControllerの白いパーツがあるだけかと思います。
この黄色いアイコンを選択してください。
右側のインスペクターで、Identity InspectorのCustom Classを変更します。
ここを先ほどのVideoUploaderViewController.swiftに変更すると接続が完了します。
2. Main.storyboardに動画選択用のボタンを追加する
Main.storyboardを開いているのでついでに動画選択のボタンを設置していきます。
これをドラッグアンドドロップでViewController内におきます。
青い淵の状態だとパーツが固定されていないのでUI部品に制約(縦横のサイズやトップからの距離などのルール)を追加していきます。
基本的に必要な制約は、縦横・座標(画面に対しての位置)です。width/height
widthを設定します。
Ctrを押しながらボタンを選択、ボタン内で選択をやめるとこのようなメニューが出てきます。
ここでwidthを選択してください。
赤くなっているのは無視してください。(ちゃんと制約が指定できたら青くなります!)
制約部分を選択すると右側にAttributes Inspectorが開きます。
ここでConstantを80にしてください。
同じようにして、heightのConstantを80で指定してください。
座標
次にボタンの座標を決めていきます。
画面のトップやボトムから距離を決めたり、画面の真ん中で指定したり方法は色々あります。
今回はトップからの距離と左からの距離を指定します。
先ほどと同じように、Ctrを押しながらボタンを選択、
今度はボタンの外までカーソルを引っ張り選択を解除します。
以下のメニューが出てくるのでTop Space to Safe Areaを選択してください。
これもConstantを548に変更してください。同じように画面の左からの制約を付けていきます。
メニューを開いたら、今度はLeading Space to Safe Areaを選択してください。
ここはConstantを71に指定してください。最後にbuttonという文字をクリックして、Selectに変更してください。
これでボタンの追加は終了です。(好きに色とか付けてもらったり、位置も自由で大丈夫です。)
(指定に中途半端な値を指定しているのは、デモで作ったアプリを
AutoLayoutで実装しているのですが、そのレイアウトに近づけるためです。(起動に使う端末はXRです。)
AutoLayoutとは、UI部品同士を相対的に配置して、どのサイズの端末でも同じような配置でレイアウトを実現できる機能です。
後でソースコードを載せるので参考にしてみてください??♀️)
3. ユーザーにカメラロールの使用許可をとる実装を追加する
カメラアプリをインストールした時にこのようなモーダルに遭遇することがあると思います。
ユーザーから、フォトライブラリにアクセスする許可を取らないと、
動画を選択することができないのでこれを実装していきます。まず、Info.plistでフォトライブラリを使用する利用目的を設定します。
アプリ名のディレクトリ以下のInfo.plistを開いてください。(テスト用のディレクトリにもあるので注意してください)
Information Property Listの横にある+ボタンを選択して、Privacy - Photo Library Usage Descriptionを入力してください。
行が追加されたら、Valueの部分には利用目的を記述します。
ここまででInfo.plistの設定は終わりです。
次に、アラートを出す実装をします。
iOS11以降.plistに設定してもアクセス権限の確認が自動的に行われなくなったので
意図的にタイミングを指定してアラート表示する必要があります。
最初の画面が表示されたタイミングでアラートを表示するようにしていきましょう。最初に開かれるページになる、VideoUploaderViewController.swiftを開いてください。
今回必要なフォトライブラリへアクセスするのでPhotosというフレームワークを使用します。
Appleが用意してくれているものなのでimport Photosを記述することで使用できるようになります。
以下のように追加してください。VideoUploaderViewController.swiftimport UIKit import Photos自動生成されるviewDidLoadの下に、confirmPhotoLibraryAuthentication()という関数を作成します。
ここで、フォトライブラリの使用許可の確認を行います。VideoUploaderViewController.swiftprivate func confirmPhotoLibraryAuthentication() { }privateは、このclass内でのみ呼び出しを許可したい時に使用します。
(railsでもストロングパラメーターを記述する時に使ったことがあるかと思います。同じやつです。)この関数内で、アクセス許可をされているかの現状の確認と許可されていなかった時の挙動を記述していきます。
VideoUploaderViewController.swiftprivate func confirmPhotoLibraryAuthenticationStatus() { //権限の現状確認(許可されているかどうか) if PHPhotoLibrary.authorizationStatus() != .authorized { //許可(authorized)されていない・ここで初回のアラートが出る PHPhotoLibrary.requestAuthorization { status in switch status { //もし状態(status)が、初回(notDetermined)もしくは拒否されている(denied)の場合 case .notDetermined, .denied: //許可しなおして欲しいので、設定アプリへの導線をおく self.appearChangeStatusAlert() default: break } } } }
PHPhotoLibrary.requestAuthorization
の時に先ほどのアラートが出現します。
その結果をstatusとしてクロージャー({})内に展開します。
このアラートは基本的に一度きりなので
アプリを閉じられたりして初回(notDetermined)のままだったり
アクセスを許可していない(denied)ユーザーに対して再度設定を促すアラートを出そうと思います。appearChangeStatusAlert()という関数を作成します。
confirmPhotoLibraryAuthenticationStatus関数の下に追加してください。VideoUploaderViewController.swiftprivate func confirmPhotoLibraryAuthenticationStatus() { //略 } //ここから private func appearChangeStatusAlert() { //フォトライブラリへのアクセスを許可していないユーザーに対して設定のし直しを促す。 //タイトルとメッセージを設定しアラートモーダルを作成する let alert = UIAlertController(title: "Not authorized", message: "we need to access photo library to upload video", preferredStyle: .alert) //アラートには設定アプリを起動するアクションとキャンセルアクションを設置 let settingAction = UIAlertAction(title: "setting", style: .default, handler: { (_) in guard let settingUrl = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(settingUrl, options: [:], completionHandler: nil) }) let closeAction = UIAlertAction(title: "cancel", style: .cancel, handler: nil) //アラートに上記の2つのアクションを追加 alert.addAction(settingAction) alert.addAction(closeAction) //アラートを表示させる self.present(alert, animated: true, completion: nil) }ここまで書けたら、画面が読み込まれた際に呼び出されるviewDidLoad()から
confirmPhotoLibraryAuthenticationStatus()を呼び出してみましょう。VideoUploaderViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() // MARK: allow access to camera roll self.confirmPhotoLibraryAuthenticationStatus() }実際に起動して、動作を確認してみましょう。
Info.plistで指定した利用目的はここに出てきます!
Don't Allowを押すと先ほどタイトルなどを設定したアラートが出現します。
settingを押すと設定アプリも開けるようになってると思います。デバッグの注意点:
今回の実装では最初のアラートは初回起動時一回しか出てこないので、
再度確認したいときは、シュミレーターからアプリを削除してビルドし直してください。4. ボタンをViewControllerに接続する
ユーザーからフォトライブラリへのアクセス許可を貰えるようになったので、
ここからはフォトライブラリから動画を選択する機能を作っていきます。まずVideoUploaderViewController.swiftと先ほどstoryboardに配置したボタンを接続していきます。
Main.storyboardを開いてください。
optionを押しながら、VideoUploaderViewController.swiftを開きます。
左側にMain.storyboard、右側にVideoUploaderViewController.swiftが開けていればOKです。
storyboardでCtrを押しながらボタンを選択して、ViewcontrollerのviewDidLoadの上まで
カーソルを引っ張ってください。
選択をやめると、このようなメニューが出てくるかと思います。
以下のようにConnectionをAction
に変更して、
NameにdidTapSelectButton
と入力してConnectを押してください。
ConnectionにAction
を指定することで
ボタンをタップされた後に呼び出される関数を自動生成することができます。
ここまでで、VideoUploaderViewController.swiftと動画選択のボタンの接続は終わりです。
5. ボタンが押されたときにフォトライブラリを開いて動画を選択する
ここでやることは、UIImagePickerControllerを使って
フォトライブラリから動画を選択できるようにします。UIImagePickerControllerとは、
メディア周り(フォトライブラリにアクセスしたりやカメラを起動したり)の機能を簡単に扱えるようにするクラスです。Photosフレームワークをインポートしてあるだけで使用できます。
(今回はフォトライブラリのアクセス許可をとる時に既にインポートしてあるのですぐに使えます。)早速実装していきます。
VideoUploaderViewControllerクラスに、UIImagePickerControllerインスタンスを生成します。VideoUploaderViewController.swiftclass VideoUploaderViewController: UIViewController{ let imagePickerController = UIImagePickerController() //略 }次に、VideoUploaderViewController.swiftにselectVideo関数を作成します。
ここにフォトライブラリを開いて動画を選択する機能を実装していきます。VideoUploaderViewController.swiftprivate func appearChangeStatusAlert() { //略 } //ここから private func selectVideo() { //選択できるメディアは動画のみを指定 self.imagePickerController.mediaTypes = ["public.movie"] //選択元はフォトライブラリ self.imagePickerController.sourceType = .photoLibrary //実際にimagePickerControllerを呼び出してフォトライブラリを開く self.present(self.imagePickerController, animated: true, completion: nil) }最後に、自動生成したdidTapSelectButtonの関数からselectVideo()を呼び出すことで
ボタンがタップされた時にフォトライブラリを開き動画を選択することができるようになります。VideoUploaderViewController.swift//選択ボタンがタップされた時に呼び出される @IBAction func didTapSelectButton(_ sender: Any) { selectVideo() }ビルドして確認してみましょう。動画を選択できるようになったと思います。
6. 選択された動画のサムネイルをViewControllerに表示する
最後に選択した動画のサムネイルをVideoUploaderViewControllerに表示します。
少しやることが多いので、流れをまとめます。以下の通りです。
- Main.storyboardにサムネイルを表示するためのUIImageViewを置く
- UIImageViewをViewControllerに接続する
- ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる
- imagePickerControllerが閉じられる時に呼び出される関数を実装する
- 取得した動画のパスを元にサムネイルを生成するMain.storyboardにサムネイルを表示するためのUIImageViewを置く
Main.storyboardを開いてください。
今回はサムネイル(画像)を表示することが目的です。画像を表示するにはUIImageViewを使用します。
UIButtonの時と同じように、LibraryからUIImageViewを検索して追加します。
高さはConstantを250ポイントに指定してください。
横幅は画面いっぱいに指定してください。
画面いっぱいにするには、Ctrを押しながらUIImageViewを選択してカーソルをUIImageViewの外側まで持っていきます。
ここで選択を解除すると以下のメニューが出てきます。
Equal Widthsを選択すると画面に対して横幅がいっぱいになります。次に横軸の座標を指定します。今回は画面に対して中央にします。
同様にメニューを出してCenter Horizontally in Safe Areaを指定してください。
このように真ん中にUIImageViewが移動すると思います。
最後に縦軸の座標を指定します。今回は動画選択のボタンのトップよりUIImageViewのボトムが54ポイント上になるように指定します。
Ctrを押しながら、UIImageViewを選択してください。カーソルは選択ボタンまで持っていってメニューを表示させます。
ここでVertical Spaceを選択してください。
Constantは54ポイントを指定してください。UIImageViewをViewControllerに接続する
選択ボタンの時と同じようにMain.storyboardを開いて、隣にVideoUploaderViewcontroller.swiftを開いてください。
(optionを押しながらVideoUploaderViewcontroller.swiftを選択してください)
Ctrを押しながら、UIImageViewをVideoUploaderViewcontroller.swiftに繋いでください。
今度はConnectionはOutlet
のままで、Connectしてください。
以下のコードが生成されたら接続は完了です。
ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる
ここが一番わかりづらいと思います?
そもそもDelegate(デリゲート)とはSwiftでよく使われる考え方で、ここら辺の記事がわかりやすいです。
【swift】イラストで分かる!具体的なDelegateの使い方。
簡単にいうと、あるクラスが別のクラスに処理をまかせる(委譲する)ことです。
なんで批准という表現が使われるかという話は、例え話がわかりやすいです。
今回扱うデリゲートやプロトコルは、所謂条約のような決まり事です。
必ず守らなければならないルールがあったり、出来ることが増えたりします。
またそこには加盟する国々(ViewControllerなど)がいます。
この関係を国々が条約に批准するというように、
ViewControllerもデリゲートやプロトコルに批准すると考えると理解しやすいです!(個人談)今回は、デリゲートに批准することで、VideoUploaderViewControllerに
UIImagePickerControllerの関数を使って、選択した動画やサムネイルを受け取ってもらいます。実装方法は、まずクラスを定義している部分にUIViewControllerと同じように必要なDelegateを記述します。
次に、imagePickerControllerの代わりにVideoUploaderViewControllerが役割を肩代わりしますよ!って宣言をします。
VideoUploaderViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() self.imagePickerController.delegate = self //略これで批准完了です!
imagePickerControllerが閉じられる時に呼び出される関数を実装する
まず、選択された動画のurlを保持するための変数を定義します。
VideoUploaderViewController.swiftclass VideoUploaderViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { //ここを追加 private var videoUrl: NSURL?先ほどのデリゲートに批准したので、動画を選択した後imagePickerが閉じられる時に呼び出される関数を利用できるようになります。
VideoUploaderViewController.swift//imagePickerが閉じられる時に呼ばれる func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { //キーを指定して選択された動画のパスを取得する let key = UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerMediaURL") videoUrl = info[key] as? NSURL //動画の絶対パスを元にサムネイルを生成(generateThumbnailFromVideoの実装は後述します。) //先ほど接続したthumbnailImageViewのimageにサムネイルをセット thumbnailImageView.image = generateThumbnailFromVideo((videoUrl?.absoluteURL)!) //サムネイルの縦横比を変えずに長い辺を画面サイズに合わせる設定 thumbnailImageView.contentMode = .scaleAspectFit //imagePickerControllerを閉じる imagePickerController.dismiss(animated: true, completion: nil) }取得した動画のパスを元にサムネイルを生成する
先ほど後述すると書いたgenerateThumbnailFromVideoという関数を実装します。
VideoUploaderViewController.swiftprivate func generateThumbnailFromVideo(_ url: URL) -> UIImage? { //以下の3行で縦動画から画像を取り出しても横向きの画像にならないようにしてる let asset = AVAsset(url: url) let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true //切り取るタイミングの指定 var time = asset.duration time.value = min(time.value, 2) //サムネイルの生成 do { let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil) return UIImage(cgImage: imageRef) } catch { return nil } }この関数は
-> UIImage?
でサムネイルを返り値として指定しています。
生成に成功するとreturn UIImage(cgImage: imageRef)
で画像を返してくれます。
これが先ほど実装した関数のこの部分に返るのでサムネイルが表示されるようになります。
thumbnailImageView.image = generateThumbnailFromVideo((videoUrl?.absoluteURL)!)
ビルドして確認してみましょう!
動画選択後にサムネイルが表示されてればOKです。
動画選択の機能は以上です。
動画再生の機能を作成する
ここでやること
1. 動画再生用ボタンを設置する
2. ViewControllerに接続する
3. 動画再生の関数を実装する1. 動画再生用ボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画再生用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に対してcenter
こんな感じです(急に色付きでスミマセン?)
2. ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
3. 動画再生の関数を実装する
動画を再生するにあたって、再生プレーヤーが必要となります。
Appleが提供しているAVKitフレームワークに、
ビデオコンテンツを再生するためのインターフェイスが用意されているのでこれを使用していきます。VideoUploaderViewController.swiftimport UIKit import Photos //ここを追加 import AVKitVideoUploaderViewController.swiftprivate let imagePickerController = UIImagePickerController() //ここを追加 private let playerViewController = AVPlayerViewController()次に、動画再生を行う
playVideo(from url: URL)
を実装します。VideoUploaderViewController.swiftprivate func playVideo(from url: URL) { //プレイヤーに受けとったurlをセット let player = AVPlayer(url: url) //先ほど初期化したplayerViewControllerのプレイヤーに上記のプレイヤーをセット playerViewController.player = player //playerViewControllerの表示・再生 self.present(playerViewController, animated: true) { print("playing video") self.playerViewController.player!.play() } }最後に再生ボタンを押された後に
playVideo(from url: URL)
を呼び出します。VideoUploaderViewController.swift@IBAction func didTapPlayButton(_ sender: Any) { //選択された動画の絶対パスがオプショナル(nilの可能性がある)ので //guard(railsでいうunless)でパスがnilなら早期リターンにしてる guard let url = videoUrl?.absoluteURL else { return } playVideo(from: url) }ここまで実装できたら、ビルドして再生ボタンを押して確認してみましょう!
動画再生の機能は以上です。
動画アップロードの機能を作成する
ここでやること
1. 動画アップロード用のボタンを設置する
2. ViewControllerに接続する
3. 動画アップロードの関数を実装する1. 動画アップロード用のボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画アップロード用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に右端対してconstantを71ポイント
こんな感じです。
2. ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
3. 動画アップロードの関数を実装する
動画のアップロードを行う
uploadVideo()
を実装します。
今回アップロードするのは動画のみです。
アップロードに必要な情報は、
・選択された動画のurl
・選択された動画の名前
になります。VideoUploaderViewController.swiftprivate func uploadVideo() { //urlと名前がなければ早期リターンさせる guard let videoClipPath = videoUrl?.absoluteURL, //urlの最後がファイル名になる let videoClipName = videoUrl?.lastPathComponent else { print("not found video path or name"); return } //バックエンドにリクエストを送る //ここは次の章で実装するので(APIクライアント)エラーのままで大丈夫です。 API.postData(videoClipPath: videoClipPath, videoClipName: videoClipName) }
uploadVideo()
もアップロード用のボタンが押された時に呼び出されるようにしておきます。VideoUploaderViewController.swift@IBAction func didTapUploadButton(_ sender: Any) { uploadVideo() }この機能は、APIクライアントとバックエンドのAPIを作って完成します。
2. [Swift] APIクライアントの実装?
SwiftからRailsにデータを送信するための機能を作っていきます。が、
その前に。動画データの扱い方について
一回やったのですが動画変換はbase64だと重くてツライので、
APIのリクエストヘッダーのContent-Typeにmultipart/form-dataを指定して
動画をAppleの標準の拡張子.MOVのままバックエンドにアップロードできるように実装します。ザックリと用語の解説
base64
base64はバイナリーデータ(今回は動画)を
String(ASCIIテキスト: アスキーと読みます)に変換する方法です。
バイナリーデータをStringに変換できるのでjson形式で扱えて便利です。
有名な変換方法なので詳しい話はこちらをどうぞ!
base64ってなんぞ??理解のために実装してみた
弱点は、上でも書いた通りめちゃくちゃに重たいです。
理由はASCIIテキストに変換していく中でデータサイズが33%増加することです。
例えば、1枚66KBのサンタを変換するとこんな感じ
(ちなみに文字列はまだまだ続く。
base64は100MB未満のデータに適していると言われているのでこのサンタはちょろい方)
??base64エンコーダー
こんな感じなので動画を全部文字列で扱ったらbase64⇄動画の変換は地獄です。
(1分くらいの動画をbase64でコンソールに出力したらXcodeが固まった)ということで今回は不採用?
multipart/form-data
HTML4から導入された方法で、複合型のコンテンツをMIME Typeを指定することで形式を変えずに送信することができます。(textだったりjpegだったりいろんな形式のデータを一緒に送信することが可能ということ)
HTTPリクエストのヘッダー部分で指定するContent-typeの一種です。
(Content-typeとは「このリクエストの中身はこんな形式のデータが入ってますよ〜」って宣言。)
詳しくはこちら。[フロントエンド] multipart/form-dataを理解してみよう
今回送信するデータはvideo/quicktime(.MOVファイル)だけですが、拡張性があるのでmultipart/form-dataを指定しています。ザックリした流れ
- Alamofireのインストール
- APIClient.swiftの作成
- POSTリクエストを実装
- http通信の許可
Alamofireのインストール
APIクライアントを簡単に実装できるライブラリです。CocoaPodsでインストールします。
導入方法はこちらが詳しいので参考にしてください。(【Swift】CocoaPods導入手順)[https://qiita.com/ShinokiRyosei/items/3090290cb72434852460]
今回pod 'Alamofire', '~> 4.7.2'
を指定してください。
インストール後、Xcodeを閉じて
プロジェクトディレクトリ以下に出てくる.xcworkspaceファイルで開き直したら完了です。
APIClient.swiftの作成
Xcodeが開けたら、左側に表示されるナビゲーターからプロジェクト名のディレクトリを指定してください。
?この状態
左下の+ボタン > File... > Swift File の順で、APIClient.swiftというファイルを作ってください。
ナビゲーターのプロジェクト名のディレクトリ以下にファイルが追加されていたら完了です。POSTリクエストを実装
Alamofireを使って、APIClient.swiftにVideoのPOSTリクエストを書いていきます。
まずはAlamofireをインポートします。APIClient.swiftimport AlamofireAPIクライアントを書いていきます。
ここでやりたいのは、
- Content-type: multipart/form-dataでリクエストを作成
- 動画データの追加
- 送信(エラーハンドリング)
です。APIClient.swiftstruct API { //APIのエンドポイント。 static let baseUrl = URL(string: "http://localhost:3000/api/v1/videos")! static func postData(videoClipPath: URL, videoClipName: String){ //multipart/form-dataでデータを送信する Alamofire.upload(multipartFormData: { multipartFormData in //multipartFormDataオブジェクトに対してデータの追加を行う //withNameはrailsのActiveStorage側で保存するときのキーと同じ multipartFormData.append(videoClipPath, withName: "clip", fileName: videoClipName, mimeType: "video/quicktime") }, to: baseUrl) { encodingResult in //encodingが成功するとこのハンドラが呼ばれる switch encodingResult { case.success(let upload, _ ,_): print(upload) upload .uploadProgress(closure: { (progress) in //進捗率の取得 print("Upload Progress: \(progress.fractionCompleted)") }) case.failure(let error): print(error) } } }こんな感じでPOSTリクエストの実装は完了です。
http通信の許可
http通信を許可する
iOS9以降、意図的にドメインを許可する設定をしないとXcodeでhttp通信できなくなりました。
設定しないと以下のようなエラーを吐きます。。The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.今回、rails側のlocalhostを叩きたいのでこの設定が必要になります。
この記事がわかりやすいので設定してみてください??
【swift】XcodeでiOSアプリのhttp通信を許可する方法
[おまけ]
ここではバックエンドやフロントエンドでCORSを指定する時でいう
ワイルドカードの指定と同じようなことをやっているので(全ドメインを許可)
本番環境で必要になる場合は(herokuデプロイとか)Exception Domainで特定のドメインのみを許可してください。
iOS9でHTTP通信ができない時の解決法以上でAPIクライアントの実装は終わりです!
3. [Rails] APIサーバーの実装?
ザックリした流れ
- プロジェクトの作成
- モデルの作成
- コントローラーの作成
- ルーティングの作成
プロジェクトの作成
環境構築は割愛??
1. ターミナルを開いて$ cd /path/to/プロジェクトを作成したいディレクトリ
で移動します
2.$ rails _5.2.3_ new video_uploader_server --api --skip-test
を実行します(アプリ名はvideo_uploader_serverを好きな名前に変更してください)
オプション 内容 _5.2.3_
railsのバージョン指定(6以降だと色々面倒なので今回はこちら) --api APIモード --skip-test 今回テスト書きません。書く方は抜いてください その他optionはrails newの書き方について徹底解説!を確認してみてください。
モデルの作成
モデルを作成します。
今回必要なのは
- Videoモデル
- ActiveStrangeで使用するモデル
です。ActiveRecordのVideoモデルの作成
$ cd アプリ名
作成したプロジェクトに移動$ rails g model video
でVideoモデルを作ります(gはgenerateのgです。今回カラム使わないのでこれだけで大丈夫です。)ActiveStorageの設定を行うのでマイグレーションはまだ行わなくて大丈夫です!
続きをどうぞ!ActiveStorageの設定
今回扱うのは動画(.MOV)なのでActiveStorageを使って保存していきます。
Active Storage の概要に詳しく解説されているので参考にしてください。
1.$ rails active_storage:install
ログはこんな感じになります
2.$ rails db:migrate
でデータベースを作ります
3. お好きなエディタでプロジェクトファイルを開きます
4.video.rb
を開いて編集しますVideoモデルに紐付ける動画ファイルをclipという名前でActiveStorageから呼び出せるように指定します。
video.rbclass Video < ApplicationRecord #ここを追加 has_one_attached :clip endこれはActiveStoregeで使用するモデルとVideoモデルのリレーションを定義しています。
Videoモデルにカラムを追加しなくてもVideoモデルに動画を保存しているかのように
ActiveStorageで動画を保存することができるようになります。コントローラーの作成
$ rails g controller api/v1/videos
でコントローラーを作成します
v1はバージョン1という意味です。
このように階層を分けてAPIのバージョンを管理するプロジェクトが多いです。
今回は簡単シンプルな構成で機能追加も想定していないので無くても大丈夫です。videos_controller.rbを編集します
videos_controller.rbclass Api::V1::VideosController < ApplicationController # videoの保存 def create video = Video.save(video_params) if video.save render json: { status: 'ok' } else render json: { status: 'ng' } end end # videoの取得 def index # とりあえず最後に保存したもの一件だけを表示 video = Video.last.clip # ActiveStorageで保存したビデオのurlをjsonで返す url = url_for(video) render json: { url: url } end private def video_params params.permit(:clip) #clipはActiveStorageに保存する時のキー end endルーティングの作成
controllerの階層を考慮した上でvideos_controllerのルーティングが設定できれば問題ないです。
routes.rbRails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html namespace 'api' do namespace 'v1' do resources :videos end end endこんな感じ。今回使わないアクションが多いのでonlyで絞り込んでもらってもいいです。
これでルーティングの実装も終わりです。
Xcodeでアプリをビルドして、railsもサーバーを起動してアップロードの確認をしてください。[ちなみに]
swiftからのリクエストは、rails側のコンソールでこんな感じで受け取ってるのを確認できます。Parameters: {"data"=>"testVideo", "clip"=>#<ActionDispatch::Http::UploadedFile:0x00007f982a2e9c70 @tempfile=#<Tempfile:/var/folders/rk/n8_pgb7x3j18qzjq1g4_4j380000gn/T/RackMultipart20191204-82817-n4evni.MOV>, @original_filename="34153FC2-F496-4B63-B0EC-D005AA1BC8DB.MOV", @content_type="video/quicktime", @headers="Content-Disposition: form-data; name=\"clip\"; filename=\"34153FC2-F496-4B63-B0EC-D005AA1BC8DB.MOV\"\r\nContent-Type: video/quicktime\r\n">}ターミナルで
open /var/folders/rk/n8_pgb7x3j18qzjq1g4_4j380000gn/T/RackMultipart20191204-82817-n4evni.MOV
で送られてきた動画の再生ができたりします。4. [Swift] 動画プレイヤーの実装?
ザックリした流れ
- 最新の動画再生用ボタンを設置する
- ViewControllerに接続する
- 最新の動画をフェッチするリクエストを実装する
- 最新の動画を再生する
動画アップロード用のボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画アップロード用のボタンを設置してください。
・横幅は文字の長さ。高さは文字の大きさを24ポイント
・縦の座標は再生ボタンから42ポイント下
・横の座標は画面に右端対してcenter
こんな感じです。
(画像追加します!)ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
(画像追加します!)最新の動画をフェッチするリクエストを実装する
APIClient.swiftに以下を追加してください。
APIClient.swift//completionを使うことで呼び出し側でリクエストと動画再生を同期的に扱えるようにしてる static func fetchLatestVideoUrl(completion: @escaping (URL) -> ()) { //レスポンスの型。今回はurlのみ struct FetchResult: Codable { let url: String } //今回パラメーターは特に必要ないので[:](空)で Alamofire.request(baseUrl, method: .get, parameters: [:]) .responseJSON { response in switch response.result { case .success: print("Success!") //レスポンスをFetchResultに変換する guard let data = response.data, let result = try? JSONDecoder().decode(FetchResult.self, from: data), //取得できたFetchResultオブジェクトのurl(String?)からURLを生成 let fetchedUrl = URL(string: result.url) else { return } //取得できたURLをcompletionに渡す completion(fetchedUrl) case .failure: print("Failure!") } } }最新の動画を再生する
最新の動画を再生するための関数
playUploadedLatestVideo()
を実装します。VideoUploaderViewController.swiftprivate func playUploadedLatestVideo() { //バックエンドからファイルのurlを返してもらう(http://localhost:3000で始まるもの) //先ほどのcompletionはここの{}(クロージャ)。この中でurlを受け取る API.fetchLatestVideoUrl() { url in //このurlを使ってプレビューと同様にplayVideo(from url: URL)でビデオを開く self.playVideo(from: url) } }最後にボタンが押されたタイミングで
playUploadedLatestVideo()
を呼び出さすように記述して完成です!VideoUploaderViewController.swift@IBAction func didTapLatestVideoButton(_ sender: Any) { playUploadedLatestVideo() }あとがき
お疲れ様でした!
今回は最低限の機能実装でした。
エラーハンドリングしてアラート出したり、一覧ページを作ったり、
削除機能つけたりお好みでいろいろ追加してみてください!あとそもそもアプリをストアにあげるとかですね!
気になったこととかもっといいやり方あるよとかご指摘は
お気軽にバシバシください??読んでくださりありがとうございました?
- 投稿日:2019-12-05T10:56:28+09:00
Rails 絵文字を登録〜Mysql2::Error: Incorrect string value〜
Railsで作ってるフォームに絵文字を登録しようとして
Mysql2::Error: Incorrect string value
と怒られた。
絵文字の形式が対応してないよーってことらしく、utf8
をutf8bm4
に変換すれば解決するらしかった。database.yml
utf8になっていたらutf8mb4に変換!
database.ymldefault: &default adapter: mysql2 encoding: utf8mb4 charset: utf8mb4 collation: utf8mb4_bin pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: port: <%= ENV['DB_PORT'] || 3306 %> reconnect: trueDB
mysql
のなかでmysql> SHOW VARIABLES like 'char%'; +--------------------------+----------------------------------------------------------+ | Variable_name | Value | +--------------------------+----------------------------------------------------------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | utf8mb4 | | character_set_server | utf8mb4 | | character_set_system | utf8 | | character_sets_dir | /usr/local/Cellar/mysql@5.7/5.7.28/share/mysql/charsets/ | +--------------------------+----------------------------------------------------------+ 8 rows in set (0.02 sec)これはもう変更後なんだけど、変更前は
utf8
祭りなのでSET character_set_database=utf8mb4;で↑のように
utf8mb4
に変換されるテーブルことにもできるはずなんだけど、何だかうまくいかなかったので全部utf8mb4にかえっちゃった
- 投稿日:2019-12-05T10:21:23+09:00
Rails 開発Tips 個人メモ
PutとPatch
PATCHとPUTはHTTPメソッド名が違うだけで役割は同じ。rails3まではPUTrils4からPATCHに移行。
Viewには繰り返しの構文使うな
Modelクラスでインスタンスmethodなどを定義してViewで使ってViewクラスの見た目をすっきりさせよう。
form_for内で用いられるf.○○タグ名 :カラム名について
<%= f.text_field :name %> <input id="モデル名_name" name="モデル名[name]" type="text" size="モデルで設定したsize" type="text">上と下は同じ
params.require
requireメソッドはストロングパラメーターのメソッド。
ストロングparameterを使うと以下のように邪魔なparameterがいっぱいついてきます。
Parameters: {"utf8"=>"✓", "authenticity_token"=>"sdva", "user"=>{"name"=>"たけし", "email"=>"sdfasfdjas"}}
ここで
params.require(:user)
とすることでuserキー以下のパラメーターのみを取得できます。=> {"name"=>"たけし", "email"=>"sdfasfdjas"}}
Update
user = User.find_by(id: 5) user.name = 'Yuta' user.saveを以下のようにするとすっきりする
'''ruby
user = User.find_by(name: 'David')
user.update(name: 'Dave')
```image.attached?
ActiveStorageを使って画像のアップロードがされているかどうかを確認するためのメソッド
- 投稿日:2019-12-05T09:17:31+09:00
Ruby on Rails 6のDockerでの環境構築
Ruby on Rails Advent Calendar 2019 5日目の記事になります。
Ruby on Rails 4.2のみ経験者でちょっと触れただけですが、3年ぶりくらいに環境構築してみました。
本当は環境構築だけじゃなくて弄くり回しかったところですが、環境構築だけでタイムアップでした。【Rails】Rails 6.0 x Docker x MySQLで環境構築
zazk/Rails-6-Docker-Alpine
Quickstart: Compose and Rails
を大いに参考にさせていただきました。環境
- macOS Mojave
- Docker for Mac
- Ruby on Rails 6.0
- PostgreSQL 11.1
初期状態のディレクトリ構成
. ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── entrypoint.sh └── docker-compose.ymlDockerfileの設定
FROM ruby:2.6 RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ && apt-get update -qq \ && apt-get install -y nodejs yarn \ && mkdir /myapp WORKDIR /myapp COPY Gemfile /myapp/Gemfile COPY Gemfile.lock /myapp/Gemfile.lock RUN bundle install COPY . /myapp COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"]docker-compose.ymlの設定
docker-compose.ymlversion: "3" services: db: image: postgres:11.1-alpine web: build: . command: bundle exec rails server -b 0.0.0.0 volumes: - ./:/myapp ports: - "3000:3000" depends_on: - dbGemfileの設定
source 'https://rubygems.org' gem 'rails', '~>6'Gemfile.lockの作成(空のファイル)
$ touch Gemfile.lockentrypoint.shの作成
entrypoint.sh#!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@"一旦Buildしてrails newでアプリ作成
$ docker-compose build $ docker-compose run web rails new . --force --webpack --database=postgresql再度Build
$ docker-compose build
./config/database.yml
の編集database.yml・ ・ ・ default: &default adapter: postgresql encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> host: db username: postgres development: <<: *default database: postgres ・ ・ ・docker-compose up を実行してサーバー立ち上げ
$ docker-compose upアクセス
0.0.0.0:3000にアクセスすると
苦戦したところとか
- webpackerはyarn必須で辛い。というかyarnがないとそもそもサーバーすら立ち上がらないのか...
- 依存関係が複雑すぎてbundle installでコケまくる
- とりあえず公式ドキュメント読んだ方がいい。Alpine Linux難しい。
明日は@jkr_2255さんの記事です!
- 投稿日:2019-12-05T03:56:17+09:00
【Rails】bundle installをしたら大量のファイルを作る問題
初めに
bundle installをした際にgithubDesktopに新しいファイル生成10000ファイルのようにたくさんのファイルを生成したことはあるでしょうか。この問題に時間を使ってしまったので書き残します。
結論
bundle installをした際に大量にファイルが生成されるのは問題ではないです。正常な挙動です。本来、railsのgemのシステムは大量のファイルの上で成り立っておりそのファイルの数を普段見ることはないですがgithubDesktopでその大量の変更を見ることができるのが問題と言えるでしょう。
原因
私は普段はgem fileにgemを追加した後に
bundle install
とターミナルに打つことでinstallをしているのですが、qiitaの記事を真似していてbundle install --path vendor/bundle
と入力しました。この後に続く表記はbundle installしたファイルをどこに保存するのかということを指定しているもので本来は毎回このように打つことが正しいようです。ただしこのように打ったことでgithubの管理下に置かれてしまったらしく、省略してbundle install
をしても大量のファイルがgithubDesktop上に表示されるようになりました。解決
.bundle
とvendor/
を一度ディレクトリから消すことで解決できます。
もう一度bundle install
をしたら元の挙動に戻ると思います。
- 投稿日:2019-12-05T01:34:22+09:00
【Rails】ユーザーのフォロー機能その3 フォローボタンの実装【Rails Tutorial 14章まとめ】
フォローボタンの実装
Relationshipsコントローラ
フォロー/フォロー解除ボタンが機能するように、Relationshipsコントローラを作成する。
$ rails generate controller Relationships
Relationshipsコントローラに必要なのは、フォロー用のcreateアクションと、フォロー解除用のdestroyアクションだけである。
よって、ルーティングは以下のようになる。config/routes.rbRails.application.routes.draw do . . . resources :users do member do get :following, :followers end end resources :microposts, only: [:create, :destroy] resources :relationships, only: [:create, :destroy] endcreate/destroyアクション
createアクションとdestroyアクションはやはりログインしていなければアクセスできないので、テストを書いてフィルターをつける。
test/controllers/relationships_controller_test.rbrequire 'test_helper' class RelationshipsControllerTest < ActionDispatch::IntegrationTest test "create should require logged-in user" do assert_no_difference 'Relationship.count' do post relationships_path end assert_redirected_to login_url end test "destroy should require logged-in user" do assert_no_difference 'Relationship.count' do delete relationship_path(relationships(:one)) end assert_redirected_to login_url end endapp/controllers/relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create user = User.find(params[:followed_id]) current_user.follow(user) redirect_to user end def destroy user = Relationship.find(params[:id]).followed current_user.unfollow(user) redirect_to user end endcreate/destroyアクションでは、フォロー/フォロー解除フォームから送信された値から、followed_idに対応するユーザーを見つけ、それにfollow/unfollowメソッドを使っている。
Ajax
Ajaxを使った非同期リクエスト
現在の仕様では、フォローボタンを押した後はプロフィールページにリダイレクトされるようになっている。
ここで、ページを維持したままリクエストを送るために、Ajaxを使用する。form_forにremote: trueを与える。
app/views/users/_follow.html.erb<%= form_for(current_user.active_relationships.build, remote: true) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: "btn btn-primary" %> <% end %>app/views/users/_unfollow.html.erb<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }, remote: true) do |f| %> <%= f.submit "Unfollow", class: "btn" %> <% end %>次に、create/destroyアクションにrespond_toメソッドを使う。
app/controllers/relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create @user = User.find(params[:followed_id]) current_user.follow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end def destroy @user = Relationship.find(params[:id]).followed current_user.unfollow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end endここではインスタンス変数を使うことに注意する。
また、ブラウザ側でJavaScriptが無効になっていた場合 (Ajaxリクエストが送れない場合) でもうまく動くようにする。
config/application.rbrequire File.expand_path('../boot', __FILE__) . . . module SampleApp class Application < Rails::Application . . . # 認証トークンをremoteフォームに埋め込む config.action_view.embed_authenticity_token_in_remote_forms = true end end次に、JavaScriptと埋め込みRubyを使って、プロフィールページを更新する。
app/views/relationships/create.js.erb$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>"); $("#followers").html('<%= @user.followers.count %>');app/views/relationships/destroy.js.erb$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>"); $("#followers").html('<%= @user.followers.count %>');このあたりの詳細がよく分からないので、とりあえず今はスキップすることにする。
フォロー機能のテスト
フォロー/フォロー解除
Relationshipsコントローラのcreate/destroyアクションにそれぞれPOST/DELETEリクエストを送り、フォローされたユーザーの数が増減することを確認する。
test/integration/following_test.rbrequire 'test_helper' class FollowingTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other = users(:archer) log_in_as(@user) end . . . test "should follow a user the standard way" do assert_difference '@user.following.count', 1 do post relationships_path, params: { followed_id: @other.id } end end test "should follow a user with Ajax" do assert_difference '@user.following.count', 1 do post relationships_path, xhr: true, params: { followed_id: @other.id } end end test "should unfollow a user the standard way" do @user.follow(@other) relationship = @user.active_relationships.find_by(followed_id: @other.id) assert_difference '@user.following.count', -1 do delete relationship_path(relationship) end end test "should unfollow a user with Ajax" do @user.follow(@other) relationship = @user.active_relationships.find_by(followed_id: @other.id) assert_difference '@user.following.count', -1 do delete relationship_path(relationship), xhr: true end end endAjax使用する場合はリクエストの後にxhr: trueを追加する。
- 投稿日:2019-12-05T00:43:27+09:00
【Rails】ユーザーのフォロー機能その2 webインターフェイス【Rails Tutorial 14章まとめ】
webインターフェイス
フォロー機能が構築できたので、実際にアプリケーション上で使えるようにしていく。
フォローのサンプルデータ
seedファイルを編集して、サンプルユーザー間にフォロー・被フォロー関係を構築する。
db/seeds.rb# ユーザー User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", 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, activated: true, activated_at: Time.zone.now) end # マイクロポスト users = User.order(:created_at).take(6) 50.times do content = Faker::Lorem.sentence(5) users.each { |user| user.microposts.create!(content: content) } end # リレーションシップ users = User.all user = users.first following = users[2..50] followers = users[3..40] following.each { |followed| user.follow(followed) } followers.each { |follower| follower.follow(user) }最初のユーザーにユーザー3からユーザー51までをフォローさせ、それから逆にユーザー4からユーザー41に最初のユーザーをフォローさせる。
(11、12章をやってない場合はactivated属性を消しておく)$ rails db:migrate:reset $ rails db:seedを実行しておく。
following/followersページへのルーティング
ユーザーのプロフィールページにフォローユーザーと被フォローユーザーの人数をそれぞれ表示し、その一覧を表示するページへのリンクを付ける。
そこで、usersリソースのルーティングにfollowersページとfollowingアクションへのルーティングを追加する。
:memberメソッドを使って以下のようにする。config/routes.rbresources :users do member do get :following, :followers end end統計情報のパーシャル
フォロー/フォロワー数を表示する統計情報のパーシャルを作成する。
app/views/shared/_stats.html.erb<% @user ||= current_user %> <div class="stats"> <a href="<%= following_user_path(@user) %>"> <strong id="following" class="stat"> <%= @user.following.count %> </strong> following </a> <a href="<%= followers_user_path(@user) %>"> <strong id="followers" class="stat"> <%= @user.followers.count %> </strong> followers </a> </div>最初の行では、@user ||= current_userとしている。
このパーシャルはhomeページとユーザープロフィールページの両方で使用する。
homeページではcurrent_userを、プロフィールページではshowアクションで定義された@userをそのまま入れる。strongタグは、文字を強調するためのもので、強調をスタイルシートで指定するなら使わなくてもよい。
homeページでこのパーシャルを呼び出す。
app/views/static_pages/home.html.erb<% if logged_in? %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= render 'shared/user_info' %> </section> <section class="stats"> <%= render 'shared/stats' %> </section> <section class="micropost_form"> <%= render 'shared/micropost_form' %> </section> </aside> <div class="col-md-8"> <h3>Micropost Feed</h3> <%= render 'shared/feed' %> </div> </div> <% else %> . . . <% end %>フォロー/フォロー解除ボタンのパーシャル
プロフィールページに統計情報のパーシャルを貼る前に、フォロー/フォロー解除ボタン(フォーム)用のパーシャルを作成しておく。
app/views/users/_follow_form.html.erb<% unless current_user?(@user) %> <div id="follow_form"> <% if current_user.following?(@user) %> <%= render 'unfollow' %> <% else %> <%= render 'follow' %> <% end %> </div> <% end %>このパーシャルは自身(ログインユーザー)のプロフィールページには表示されない。
つまり、自身とは別のユーザー用である。
そのユーザーをフォローしていればフォロー解除用の、フォローしていなければフォロー用のパーシャルを表示する。各パーシャルは以下のようになる。
app/views/users/_follow.html.erb<%= form_for(current_user.active_relationships.build) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: "btn btn-primary" %> <% end %>app/views/users/_unfollow.html.erb<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }) do |f| %> <%= f.submit "Unfollow", class: "btn" %> <% end %>これらのフォームは、どちらもform_forを使ってRelationshipオブジェクトを操作している。
前者は新規のオブジェクトをPOSTリクエストでcreateアクションに、後者は既存のオブジェクトをDELETEアクションでdestroyアクションに送信している。前者ではhidden_field_tagを使って、フォローしたいユーザーのIDをfollowed_idに入れて送信している。
このhidden_field_tagは次のようなhtmlを生成している。<input id="followed_id" name="followed_id" type="hidden" value="3" />必要なパーシャルができたので、プロフィールページに統計情報のパーシャルとフォロー用のパーシャルを表示する。
app/views/users/show.html.erb<% provide(:title, @user.name) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <h1> <%= gravatar_for @user %> <%= @user.name %> </h1> </section> <section class="stats"> <%= render 'shared/stats' %> </section> </aside> <div class="col-md-8"> <%= render 'follow_form' if logged_in? %> <% if @user.microposts.any? %> <h3>Microposts (<%= @user.microposts.count %>)</h3> <ol class="microposts"> <%= render @microposts %> </ol> <%= will_paginate @microposts %> <% end %> </div> </div>フォローボタンはログイン中のみ表示するようにしている。
following/followerページ
フォロー/フォロワーを表示するページを作成する。
各ページへはログインしていないとアクセスできないようにする。
まずはテストを書く。test/controllers/users_controller_test.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end . . . test "should redirect following when not logged in" do get following_user_path(@user) assert_redirected_to login_url end test "should redirect followers when not logged in" do get followers_user_path(@user) assert_redirected_to login_url end endfollowing/followedアクションへのルーティングはすでに実装しているので、それらの中身を書いて、beforeフィルターをかける。
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers] . . . def following @title = "Following" @user = User.find(params[:id]) @users = @user.following.paginate(page: params[:page]) render 'show_follow' end def followers @title = "Followers" @user = User.find(params[:id]) @users = @user.followers.paginate(page: params[:page]) render 'show_follow' end private . . . endここで、following/followersアクションには対応するビューをそれぞれに用意せず、show_followという共通のビューを使用している。
それは、表示するユーザーの内容とタイトル以外は同じだからである。show_followビューは以下のようになる。
app/views/users/show_follow.html.erb<% provide(:title, @title) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= gravatar_for @user %> <h1><%= @user.name %></h1> <span><%= link_to "view my profile", @user %></span> <span><b>Microposts:</b> <%= @user.microposts.count %></span> </section> <section class="stats"> <%= render 'shared/stats' %> <% if @users.any? %> <div class="user_avatars"> <% @users.each do |user| %> <%= link_to gravatar_for(user, size: 30), user %> <% end %> </div> <% end %> </section> </aside> <div class="col-md-8"> <h3><%= @title %></h3> <% if @users.any? %> <ul class="users follow"> <%= render @users %> </ul> <%= will_paginate %> <% end %> </div> </div>following/followerページのテスト
show_followページが正しく描画されているかの統合テストを作成する。
$ rails generate integration_test following
Relationshipのfixtureファイルを書いて、テスト用ユーザーにフォロー/被フォローの関係を設定する。
test/fixtures/relationships.ymlone: follower: michael followed: lana two: follower: michael followed: malory three: follower: lana followed: michael four: follower: archer followed: michael前半の2つでMichaelがLanaとMaloryをフォローし、後半の2つでLanaとArcherがMichaelをフォローしている。
統合テストは以下のようになる。
test/integration/following_test.rbrequire 'test_helper' class FollowingTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) log_in_as(@user) end test "following page" do get following_user_path(@user) assert_not @user.following.empty? assert_match @user.following.count.to_s, response.body @user.following.each do |user| assert_select "a[href=?]", user_path(user) end end test "followers page" do get followers_user_path(@user) assert_not @user.followers.empty? assert_match @user.followers.count.to_s, response.body @user.followers.each do |user| assert_select "a[href=?]", user_path(user) end end endassert_not @user.followers.empty?というテストは、これがtrueだった場合に、後の
@user.followers.each do |user| assert_select "a[href=?]", user_path(user) endでエラーとならないように入れている。