- 投稿日:2020-09-14T23:59:29+09:00
instagramのクローンアプリを作る②
はじめに
タイトルの通り、簡易版instagramのアプリを作っていきます。
下記の工程に分けて記事を執筆していきますので、順を追って読んでいただけたらなと思います。①アプリ作成〜ログイン機能の実装
②写真投稿機能の実装 ←イマココ
③ユーザーページの実装
④いいね機能の実装
⑤投稿削除機能の実装Active Storageの導入
Active Storageとは...
ファイルアップロードを行うための機能で、
これを使えばフォームで画像の投稿機能などが簡単に作れます。
※以下、アプリケーションのディレクトリでターミナルrails active_storage:install続けてphotoモデルを作成します。
photo
はuser
に紐づいているのでuser:belongs_to
caption:text
とすることで、text型のカラムも作成ターミナルrails g model photo user:belongs_to caption:textそして
ターミナルrails db:migrate最後にコントローラの作成を行います。
ターミナルrails g controller photos下準備完了です。
写真投稿ページへのリンクを作成
まず、ルーティングの設定を行います。
routes.rbRails.application.routes.draw do root 'homes#index' devise_for :users resources :photos # ←ここ end次にホーム画面を編集していきます。
index.html.erb<h3>home</h3> <div> <%= link_to 'logout', destroy_user_session_path, method: :delete %> </div> <div> <%= link_to '写真投稿', new_photo_path %> </div>
new_photo_path
はrails routesのPrefixで確認
photosモデルに
new.html.erb
を作成し、確認用に下記のように記述してみます。app/views/photos/new.html.erb<h3>写真投稿</h3>下記のように、ホーム画面から写真投稿のページに遷移できていれば成功です。
コントローラの設定
rails g controller photos
で作成したコントローラに記述していきます。photos_controller.rbclass PhotosController < ApplicationController before_action :authenticate_user! def new @photo = current_user.photos.new end def create @photo = current_user.photos.new(photo_params) if @photo.save redirect_to :root else render :new end end private def photo_params params.require(:photo).permit(:caption, :image) end end
before_action :authenticate_user!
で、
ログインユーザーのみ投稿できるように設定しています。
createアクション
で引数(photo_params)
とし、
private
以下で(photo_params)
を定義しています。また
createアクション
で
保存に成功すればホーム画面に、
失敗すれば新規投稿画面に戻る(留まる)よう設定しています。新規投稿画面のviewを編集
その前に、
userモデル
とphotosモデル
のアソシエーションを確認します。
userとphotosは1対多の関係なので、以下のように編集します。user.rbclass User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :photos # ←ここ endphoto.rbclass Photo < ApplicationRecord belongs_to :user # ←前回記述した箇所 has_one_attached :image # ←ここ end
has_one_attached :カラム名
はモデルに1つの画像を紐づける場合に使います。
複数の画像を紐づけるときはhas_many_attached :カラム名
です。今回は1つの画像なので
has_one_attached
としています。
ちなみにoneとmanyで、画像を表示させる際の記述の仕方が異なります。has_one_attached<%= image_tag(@photo.image) %>has_many_attached<% images.count.times do |i| %> <%= image_tag(@photo.image[i]) %> <% end %>
has_many_attached :カラム名
だと、
imageは配列として格納されるので上記のようになります。前置きが長くなりましたが、新規投稿画面のviewを編集していきます。
app/views/photos/new.html.erb<h3>写真投稿</h3> <%= form_with model: @photo, local: true do |f| %> <div> <%= f.file_field :image %> </div> <div> <%= f.text_area :caption %> </div> <%= f.submit %> <% end %>投稿された画像をホーム画面に表示させるようにします。
※とりあえずの確認用なので、後々修正していきます。index.html.erb<h3>home</h3> <div> <%= link_to 'logout', destroy_user_session_path, method: :delete %> </div> <div> <%= link_to '写真投稿', new_photo_path %> </div> <% current_user.photos.each do |photo| %> <div> <p><%= photo.caption %></p> <%= image_tag photo.image %> </div>each文で全ての投稿を表示させるようにしています。
新規投稿ページから
ファイル選択
、caption入力
、Create Photo
をクリックで、
以下のように投稿できていればとりあえずはカタチになっているかと思います。
以上です。お疲れ様でした。
- 投稿日:2020-09-14T22:42:46+09:00
《未経験→webエンジニア》実務1日目
この記事の目的
自分がやったこと、知らなかったこと、やるべきことを明確にし
1日あたりの成長速度を速める。「エンジニアになって、実務始まるとこんな感じなんだー」という
参考にも!【今日やったこと】
・オリエンテーション
・PCの環境設定
・入社後の書類もろもろ【知らなかったこと】
・APIテストってなに?
https://qiita.com/k-penguin-sato/items/defdb828bd54729272ad・AWSの資格について(知ってたけど、きちんと調べていなかった)
https://proengineer.internous.co.jp/content/columnfeature/13442【明日】やるべきこと
・railsの環境構築
・テストのコードが読めるよう、手を動かす
- 投稿日:2020-09-14T22:41:34+09:00
ネストした form_withの理解を深める
動機
ネストしていない場合はリダイレクト先を自動推論してくれていたので、ネストしている場合の知識のまとめとして記述
ルーティングでネストを定義している時の記述例
前提
userモデル・boardモデル・taskモデルがあり
boardはuserに紐付いている
taskはuserとboardに紐づいている書き方
controllerファイル
def new @board = Board.find(params[:board_id]) @task = @board.tasks.build end def edit @board = Board.find(params[:board_id]) @task = @board.tasks.find(params[:id]) endポイント
タスクはどれかのボードに紐づいており、
どのボードに書き込まれているタスクなのかが情報として必要なので、まずどのボードにあるかを取得し、そのタスクを探す
viewファイル
<%= form_with model: [@board, @task] do |form| %> <%= form.text_field :text %> <%= form.submit %> <% end %>modelの引数は配列として渡す
[(親)@インスタンス変数, (子)@インスタンス変数]の順で記述するようにする
- 投稿日:2020-09-14T21:18:13+09:00
Rails pry-byebugの使い方
Dockerを使用したRails開発におけるpry-byebugの使い方
自分への備忘録です。
手順
- Railsコンテナへのアタッチ
- ソースコードのデバッグ位置に"binding.pry"
- 任意のコード
- ソースコードの修正後、アタッチの解除
1.Railsコンテナへのアタッチ
Railsコンテナ名の確認
App $ docker psApp $ docker attach *AppName2. ソースコードのデバッグ位置に"binding.pry"
boards_controller.rbclass BoardsController < ApplicationController def index end def new @board = Board.new binding.pry end end3. 任意のコード
[1] pry(#<BoardsController>)> @board => #<Board:0x00005613d01ef1b0 id: nil, name: nil, title: nil, body: nil, created_at: nil, updated_at: nil> [2] pry(#<BoardsController>)> @board.name => nil [3] pry(#<BoardsController>)> @board.name = 'Takuma' => "Takuma" [4] pry(#<BoardsController>)> @board => #<Board:0x00005613d01ef1b0 id: nil, name: "Takuma", title: nil, body: nil, created_at: nil, updated_at: nil> [5] pry(#<BoardsController>)>4. ソースコードの修正後、アタッチの解除
コード修正後, Ctrl + p → q
- 投稿日:2020-09-14T20:54:28+09:00
【Rails】オブジェクトの中身を確かめる
オブジェクトとは?
オブジェクトは「箱」のようなもの。
そこで、存在チェックをするときには、
- その箱の中身があるのか?
- そもそも箱そのものがあるのか
という観点で見ていく。
箱そのものは存在していないのか? nil?
obj.nil?箱の中身は空なのか? empty?
obj.empty?箱そのものは存在していないのか?してても中身は空なのか? blank?
「箱が存在していないか、または中身が存在していない状態」
ちなみに、Railsのみのメソッドobj.blank? # 同義 obj.nil? || obj.empty?箱そのものは存在しているし、かつ中身も空ではないか? present?
「箱もある、かつ中身もある状態」
ちなみに、Railsのみのメソッドobj.present? #同義 !obj.nil? && !obj.empty? obj.blank?
- 投稿日:2020-09-14T17:03:43+09:00
CapistranoでGit LFSを使う
Git LFSで管理されたファイルがあるリポジトリをcapistrano(3.14.1)で配備してもリンクのまま配備されてしまいます。ホスト側にgit-lfsをインストールしてみたのですが結果は変わらずでした。
という訳で当面の回避策です。まずは、目的のファイル(例:
public/foo.mp4
)をrsyncやscpコマンドなどで、capistranoのsharedフォルダにコピーしておきます。rsync -avz public/foo.mp4 host:myproject/shared/public/sharedフォルダから、配備するアプリにファイルをコピーするコマンドは以下のように記述します。
config/deploy.rbappend :linked_files, "public/foo.mp4"ローカル環境などからcurrent(例:
myproject/current/public/
)に直接コピーしても、その場では動きますが、次のリリースを配備するときにディレクトリごと置き換わってしまうため、この一手間が必要です。
- 投稿日:2020-09-14T14:52:03+09:00
特定のページでJavaScriptを動作させる方法
動作環境
Ruby 2.6.5
Rails 6.0.3.2基本的にJavaScriptは全てのページで発生しているため、JavaScriptを使用していないページでも検証ツールのconsole内でJavaScriptのエラーが発生してしまいます。それが、ようやく解決できたので、投稿してみました。
実装に必要なコード
hoge.jsif (location.pathname.match("hoge")){ //ここからJavaScriptを書き始める。 }これでhogeというパスでのみhoge.jsは動作します。
一応解説を入れておくと、location.pathnameにより現在のパスを取得しmatchでhogeと合っているのかを確認しています。
個人的にconsole内のJavaScriptのエラーが気になっていたので、これですっきりしました。
- 投稿日:2020-09-14T13:56:07+09:00
Rspec createアクションを使わずにデータの増減をテストする
この記事について
下記の参考URLのようなcreateアクションを使わずにデータの増減をテストしたかったが、記事が少なかったので投稿
https://qiita.com/t2kojima/items/ad7a8ade9e7a99fb4384#postcreate参考記事
https://stackoverflow.com/questions/50630315/rspec-count-change-by
コード
let
でデータを作成して、その作成が成功したかしてかのコードを書いてやれば成功できる。let(:book) { Book.create(title: "テストタイトル") } let(:another_book) { Book.create(title: "") } describe 'Book' do context 'Bookのデータ登録' do it 'データの登録に成功する' do expect { book }.to change { Book.count }.by(1) end it 'データの登録に失敗する' do expect { another_book }.to change { Books.count }.by(0) end end end
- 投稿日:2020-09-14T11:19:32+09:00
任意の文字列が左から何文字目に出てくるかを出力したい
【概要】
1.結論
2.どのように使うか
3.ここから学んだこと
1.結論
indexメソッドと、putsを使う!
2.どのように使うか
def search(str) puts(str.index("検索したい任意の文字列")+1) endとなります!
こうすることで(str)が仮引数になっており任意の文字列(実引数)を取ってくる形になり戻り値(putsの中身 )が出力されます。str.index("検索したい任意の文字列",[検索したい開始位置])
が型になります。ここで気をつけてほしいことが2点あります。
1点目に"+1"としている部分です。配列と同じようにカウントが"0"から始まります。
2点目に最初の文字しか反応しないことです。ex) ohmygoodness,oh!
であれば"oh"を検索した際に”0”と出力されてしまう(1点目)。そして2つ目の"oh"は認識されません(2点目)。
3.ここから学んだこと
”左から”があれば右からもあるわけでその場合は”rindex”を使用します。注意したいことが2点あります。1点目は[検索したい開始位置]は負の数が入ることです。"-1""-2"と記載します。
2点目は出力される値は先頭から数えた値になります。
また、こちらも最初の文字しか反応しない(今度は末尾のohのみ)ことです。参考にしたサイト:
Rubyで文字列の検索をする方法:index, rindex
- 投稿日:2020-09-14T10:49:29+09:00
RailsにDevise+OmniAuthでユーザ認証したい
今回やりたいこと
Deviseを使った基本的なユーザー認証機能
SNS認証には仮登録メールを介さずにワンクリック登録となるようにしたい
Deviseの初期設定でユーザー情報の編集をする際に逐一パスワードを求められるが、ユーザーフレンドリーではないのでパスワードを入力せずにユーザー情報を編集したい
この記事の個人的目的
備忘録。GitHubにこのプロジェクトのソースコードを残しておきます。
https://github.com/zizynonno/devise_omniauth
1 deviseの導入
1.1 プロジェクトの作成
新しいプロジェクトを作ります。
$ rails new devise_omniauth $ cd devise_omniauth1.2 Gemfileの追加とインストール
Gemfileに以下のgemを追加する。
Gemfilesource 'https://rubygems.org' (省略)... # Devise gem 'devise' gem 'devise-i18n' gem 'omniauth-twitter' gem 'omniauth-facebook' gem 'dotenv-rails'Gemfilegem 'devise' #ユーザー認証 gem 'devise-i18n' #deviseのi18n gem 'omniauth-twitter' #twitter認証 gem 'omniauth-facebook' #facebook認証 gem 'dotenv-rails' #環境変数の設定gemをインストール。
$ bundle install2 deviseの設定
devise関連ファイルを追加。
$ rails g devise:install
このコマンドを実行すると、ターミナルに英文でdeviseの設定について記載されています。
それでは1〜4まで実行していきましょう。2.1 デフォルトURLの指定
config/environments/development.rbRails.application.configure do # Settings specified here will take precedence over those in config/application.rb. (省略)... # mailer setting config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } end2.2 root_urlの指定
1番で指定したhttp://localhost:3000/にアクセスした際に表示されるページを指定します。
このプロジェクトではページを1つも作っていないため、先に追加します。Pagesコントローラーと、indexページとshowページを追加してみます。
$ rails g controller Pages index show
routes.rbに以下を指定します。
config/routes.rbRails.application.routes.draw do root 'pages#index' get 'pages/show' (省略)... end2.3 フラッシュメッセージの追加
ログインした時などに上の方に「ログインしました」みたいなメッセージが出るようにします。
以下のファイルの<body>
タグのすぐ下に指定されたタグを挿入します。app/views/layouts/application.html.erb<!DOCTYPE html> <html> <head> <title>DeviseRails5</title> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <%# ここから %> <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> <%# ここまで %> <%= yield %> </body> </html>2.4 DeviseのViewを生成
$ rails g devise:views
すると以下の様なファイルが生成されます。
app/views/devise/shared/_links.html.erb (リンク用パーシャル) app/views/devise/confirmations/new.html.erb (認証メールの再送信画面) app/views/devise/passwords/edit.html.erb (パスワード変更画面) app/views/devise/passwords/new.html.erb (パスワードを忘れた際、メールを送る画面) app/views/devise/registrations/edit.html.erb (ユーザー情報変更画面) app/views/devise/registrations/new.html.erb (ユーザー登録画面) app/views/devise/sessions/new.html.erb (ログイン画面) app/views/devise/unlocks/new.html.erb (ロック解除メール再送信画面) app/views/devise/mailer/confirmation_instructions.html.erb (メール用アカウント認証文) app/views/devise/mailer/password_change.html.erb (メール用パスワード変更完了文) app/views/devise/mailer/reset_password_instructions.html.erb (メール用パスワードリセット文) app/views/devise/mailer/unlock_instructions.html.erb (メール用ロック解除文)3 Userモデルの設定
3.1 Userモデルの作成
$ rails g devise User
を実行するとmigrationファイルとuserファイルが出来上がります。
db/migrate/20200912194315_devise_create_users.rbclass DeviseCreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t| ## Database authenticatable t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at # ## Trackable # t.integer :sign_in_count, default: 0, null: false # t.datetime :current_sign_in_at # t.datetime :last_sign_in_at # t.string :current_sign_in_ip # t.string :last_sign_in_ip # ## Confirmable # t.string :confirmation_token # t.datetime :confirmed_at # t.datetime :confirmation_sent_at # t.string :unconfirmed_email # Only if using reconfirmable # ## Lockable # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at t.timestamps null: false end end endapp/models/user.rbclass User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable end3.2 マイグレーションファイル、Userモデルの編集
これを使うものだけコメントアウトしていきます。
db/migrate/20200912194315_devise_create_users.rbclass DeviseCreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t| ## Database authenticatable t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at ## Trackable t.integer :sign_in_count, default: 0, null: false t.datetime :current_sign_in_at t.datetime :last_sign_in_at t.string :current_sign_in_ip t.string :last_sign_in_ip ## Confirmable t.string :confirmation_token t.datetime :confirmed_at t.datetime :confirmation_sent_at t.string :unconfirmed_email # Only if using reconfirmable ## Lockable t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts t.string :unlock_token # Only if unlock strategy is :email or :both t.datetime :locked_at t.timestamps null: false end add_index :users, :email, unique: true add_index :users, :reset_password_token, unique: true add_index :users, :confirmation_token, unique: true add_index :users, :unlock_token, unique: true end endマイグレーションファイルで入れるものに加え、OAuth認証をするので
omniauth_providers: [:twitter,:facebook]
を追加します。app/models/user.rbclass User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable, :timeoutable, :omniauthable, omniauth_providers: [:twitter,:facebook] end3.3 omniauth用カラムの追加
ついでにomniauth-twitter,omniauth-facebookで使う
provider
、uid
、username
をUserテーブルに追加します。$ rails g migration add_columns_to_users provider uid username
以下のようなマイグレーションファイルができます。
db/migrate/20200912194427_add_columns_to_users.rbclass AddColumnsToUsers < ActiveRecord::Migration[5.2] def change add_column :users, :provider, :string add_column :users, :uid, :string add_column :users, :username, :string end endここまで出来たら以下を実行します。
$ rake db:migrate
4 Twitter,facebookで認証する
4.1 Twitter認証,Facebook認証をするためのそれぞれのAPIキー、シークレットキーを取得する
以下よりアプリケーションを作成する。
作成が完了したら、設定より「Add Platform」→「Website」を選択する。
サイトURLにURLを入力する(例:http://localhost:3000
)。以下よりアプリケーションを作成する。
作成が完了したら、「Settings」より以下の設定を行なう。
- Callback URL
- 例:
http://〜/users/auth/twitter
- 以下にチェックを入れる:
- Allow this application to be used to Sign in with Twitter
4.2 設定ファイルの編集
それぞれのAPIキー、シークレットキーを以下の該当箇所にコピーして貼り付けます。
config/initializers/devise.rbDevise.setup do |config| # The secret key used by Devise. Devise uses this key to generate (省略)... config.omniauth :facebook, 'App IDを入力', 'App Secretを入力' #すぐに訂正します config.omniauth :twitter, 'API keyを入力', 'API secretを入力' #すぐに訂正するのでGitHubにコミットしないでください end4.3 Userコントローラにコールバック処理を実装
provider
と同じ名前のメソッドを定義する必要がある。
ただ、基本的に各プロバイダでのコールバック処理は共通しているので、callback_from
メソッドに統一している。$ rails generate devise:controllers usersapp/controllers/users/omniauth_callbacks_controller.rbclass Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController def facebook callback_from :facebook end def twitter callback_from :twitter end private def callback_from(provider) provider = provider.to_s @user = User.find_for_oauth(request.env['omniauth.auth']) if @user.persisted? flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize) sign_in_and_redirect @user, event: :authentication else session["devise.#{provider}_data"] = request.env['omniauth.auth'] redirect_to new_user_registration_url end end end4.4 ルーティング処理
以下のように、OAuthのコールバック用のルーティングを設定する。
config/routes.rbRails.application.routes.draw do devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' } # ... end5 APIキー,シークレットキーの非公開
Twitter認証,Facebook認証をするためのそれぞれのAPIキー、シークレットキーを取得しました、この情報は非常に機密性が高く決して外部に漏らしてはいけない情報です。悪用される恐れがあるため、GitHubのリモートリポジトリや本番環境でAPIキー,シークレットキーが間違って一般公開されないような処理を施す必要があります。
最初の方で環境変数を設定するための
gem 'dotenv-rails'
をbundle install
しているので、dotenv-rails
を使ったAPIキー、シークレットキーの方法を学んでいきます。5.1 .envファイルの設置
次に、環境変数を定義する.envファイルをアプリのプロジェクトルート直下に設置します。
.envファイルはdotenv-rails
をbundle install
しても自動生成されない為、下記の様にtouchコマンドを使って手動でファイルを作成する必要があります。$ touch .env5.2 .envファイルに以下を記載する
TWITTER_API_KEY="取得したTwitterAPIキー" TWITTER_SECRET_KEY="取得したTwitterシークレットキー" FACEBOOK_API_ID="取得したFacebookAPIキー" FACEBOOK_API_SECRET="取得したFacebookシークレットキー"5.3 環境変数を使う
ENV['SECRET_KEY']
のような記載方法で、ハードコーディングしているファイルに環境変数を代入していきます。config/initializers/devise.rbDevise.setup do |config| # The secret key used by Devise. Devise uses this key to generate (省略)... config.omniauth :twitter, ENV['TWITTER_API_KEY'], ENV['TWITTER_API_SECRET_KEY'] config.omniauth :facebook, ENV['FACEBOOK_API_ID'], ENV['FACEBOOK_API_SECRET'] end6 ユーザーモデルにメソッドを追加する
Userモデルにself.from_omniauthとself.new_with_sessionを作ります。
self.from_omniauthではuidとproviderで検索してあったらそれを、無かったらレコードを作ります。
self.new_with_sessionについては、もしこのメソッドを追加しておかなければ、Twitter認証後サインアップページで登録を行っても、認証情報として取ってきたuidやproviderなどが登録されません。それらが登録されないのでTwitterで認証しても登録されてないユーザーとして毎回サインアップページに飛ばされます。app/models/user.rbclass User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable, :timeoutable, :omniauthable, omniauth_providers: [:twitter] def self.from_omniauth(auth) find_or_create_by(provider: auth["provider"], uid: auth["uid"]) do |user| user.provider = auth["provider"] user.uid = auth["uid"] user.username = auth["info"]["nickname"] end end def self.new_with_session(params, session) if session["devise.user_attributes"] new(session["devise.user_attributes"]) do |user| user.attributes = params end else super end end end6.1 Userモデルにfindメソッドを実装
uid
とprovider
の組み合わせは一意であり、これによりユーザを取得する。
レコードに存在しない場合は作成する。app/models/user.rbclass User < ActiveRecord::Base # ... def self.find_for_oauth(auth) user = User.where(uid: auth.uid, provider: auth.provider).first unless user user = User.create( uid: auth.uid, provider: auth.provider, email: User.dummy_email(auth), password: Devise.friendly_token[0, 20] ) end user.skip_confirmation! #仮登録メールを介さずに即時登録 user end private def self.dummy_email(auth) "#{auth.uid}-#{auth.provider}@example.com" end endメールアドレスでの認証も実装している場合、OAuthでの認証時もメールアドレスを保存する必要がある。
ここでは、uid
とprovider
の組み合わせが一意なことを利用して、self.dummy_email
のように生成している。以下ファイルを編集して、コールバック用のコントローラーとしてさっき作ったコントローラーが呼ばれるようにします。これを書かないとdevise側のコントローラーが呼ばれます。
config/routes.rbRails.application.routes.draw do devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' } root 'pages#index' get 'pages/show' (省略)... endこれでTwitter認証ができるようになりました。
初回、Twitter認証を行うと、サインアップページに飛ばされ、そこでメールアドレスやパスワードを入力して登録するとユーザー情報が登録されます。
今回はcomfirmable機能を入れているので、登録したら確認メッセージを送ったとのメッセージが出て、そのままログインすることはできません。
この機能を入れてなかった場合、登録すると即ログインします。7 SNS認証には仮登録メールを介さずに即時登録となるようにしたい
app/model/user.rbclass User < ActiveRecord::Base # ... def self.find_for_oauth(auth) user = User.where(uid: auth.uid, provider: auth.provider).first unless user user = User.create( uid: auth.uid, provider: auth.provider, email: User.dummy_email(auth), password: Devise.friendly_token[0, 20] ) end ######これを追記!###### user.skip_confirmation! ####################### user end private def self.dummy_email(auth) "#{auth.uid}-#{auth.provider}@example.com" end end8 ユーザー情報の編集で逐一パスワードを求められるのがだるい
8.1 routes.rbを修正する
routes.rbdevise_for :users, controllers: { } Rails.application.routes.draw do devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks', registrations: 'users/registrations' } root 'pages#index' get 'pages/show' (省略)... end8.2 update_resourceメソッドをオーバーライドする
registrations_controller.rbclass RegistrationsController < Devise::RegistrationsController protected # 追記する def update_resource(resource, params) resource.update_without_password(params) end end8.3 current_passwordフォームを削除する
views/devise/registrations/edit.html.erb<div class="field"> <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br /> <%= f.password_field :current_password, autocomplete: "current-password" %> </div>こちらのフォームを削除しましょう。
これで、パスワードを入力しなくてもユーザーの登録情報を
編集することが可能になりました!9 やりたいことの文献一覧
基本的なユーザー認証機能に加え、SNS認証でワンクリック登録できる仕組みの構築。
[Rails] deviseの使い方(rails5版)
RailsにDevise+OmniAuthでユーザ認証を実装する手順環境変数を使用し、APIキーを隠してリモートにpushしたい(App Secretなどをハードコーディングしているのはよろしくない)
【Rails】dotenv-railsの導入方法と使い方を理解して環境変数を管理しよう!
環境変数の設定ダミーではなく、twitterやfacebookに登録されたemailをDBを持っていきたい(3日くらいかかる)
omniauth-twitterでemail情報を取得する
twitterのoauthを使ってみる(emailも取得)
facebookのoauthを使ってみる(emailも取得)
2015年7月9日以降にFacebook認証でメールアドレスが取れない問題とその対策
TwitterやGoogle,Githubなどの外部サイトを用いた認証には仮登録メールを介さずに即時登録となるようにしたい
Devise内でomniauthのtwitter認証が完了してもレコードが格納されない
deviseのTwitterログイン時はメール認証とメール送信をスキップする
【Rails5】SNS認証でメールアドレス介さず登録・ログインできるようにする実装deviseをi18nで日本語にしたい
i18nで日本語化ユーザー情報の編集で逐一パスワードを求められるのがだるい
[Devise] パスワードを入力せずにユーザー情報を編集するメールアドレスのみでユーザー登録を行う。
devise でメールアドレスのみでユーザー登録を行い、パスワードを後から設定する方法
How To: Email only sign upサインインする際にメールアドレス以外でサインインする方法
How To: Allow users to sign in with something other than their email address
メールアドレスのアップデートをする際に確認を必要としない方法(スキップしたい)
deviseでメールアドレスのアップデートを確認する必要はありませんか?
Deviseでメールアドレスの確認をスキップするその他
【Rails】deviseのTwitter認証で「Unauthorized 403 Forbidden」が出てしまう場合の対処法
deviseのドキュメント(英語)
- 投稿日:2020-09-14T10:41:54+09:00
instagramのクローンアプリを作る①
はじめに
タイトルの通り、簡易版instagramのアプリを作っていきます。
下記の工程に分けて記事を執筆していきますので、順を追って読んでいただけたらなと思います。①アプリ作成〜ログイン機能の実装 ←イマココ
②写真投稿機能の実装
③ユーザーページの実装
④いいね機能の実装
⑤投稿削除機能の実装まずはアプリケーションを作成
ターミナルを開いて下記コマンドを打ち込みます。
データベースはmysqlを使用していきますので、
オプションで「 -d mysql 」としています。ターミナルrails new instaclone -d mysql作成できたらエディターを立ち上げて、「 datebase.yml 」を編集します。
encoding
をutf8
に修正します。datebase.ymldefault: &default adapter: mysql2 encoding: utf8 # ←修正箇所 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: socket: /tmp/mysql.sock修正できたらターミナルに戻り、
instaclone
のディレクトリに移動してから
rails db:create
を実行します。ターミナルinstaclone % rails db:create下準備が整いましたので、次からログイン機能の実装を行います。
deviseの導入
deviseとは...
Railsで作成したアプリケーションに、
簡単に認証機能を実装することができるgemのひとつです。
ログイン、サインアップなどのログイン機能が作成出来ます。Gemfileに下記のgemを追加します。(最下部)
その後、ターミナルでbundle install
を行います。Gemfilegem 'devise'ターミナルinstaclone % bundle installgemの追加が完了したら、deviseの設定ファイルを作成します。
ターミナルinstaclone % rails g devise:install続いて、Userモデルを作成します。
ターミナルinstaclone % rails g devise Userマイグレーションファイルも作成されるので
rails db:migrate
を実行します。ターミナルinstaclone % rails db:migrateこれでログイン機能はできたので、確認用にホーム画面を作ります。
ターミナルで下記コマンドを実行し、ホーム画面用のコントローラを作成します。ターミナルinstaclone % rails g controller homes作成できたら、
homes_controller
にindexメソッドを追加し、
routes.rb
にルートの設定を記述します。homes_controller.rbclass HomesController < ApplicationController def index end endroutes.rbRails.application.routes.draw do root 'homes#index' # ←ここ devise_for :users end先ほどコントーローラを作成した時に、一緒にviewファイルも作成されています。
場所はapp/views/homes
です。
こちらに、ホーム画面用のviewファイルを作成し、表示用の文字を記述します。app/views/homes/index.html.erb<h3>home</h3>ターミナルで
rails s
を実行し、ローカルサーバーを立ち上げ、
http://localhost:3000/ で確認します。
homeと表示できていれば成功です。before_actionでログイン画面に誘導
ここまでの状態では、誰もがホーム画面にアクセスできてしまいますので、
コントローラにbefore_action
を追記します。
これで、ログイン(もしくは登録)していないユーザーは、自動的にログイン画面に飛ばされます。homes_controller.rbclass HomesController < ApplicationController before_action :authenticate_user! #←ここ def index end endこの画面で、EmailとPasswordを入力してSign upをクリックすると、
先ほどhomeと表示された画面に遷移することができます。これでほぼほぼ完成ですが、最後にログアウトするボタンを作成する必要があります。
ログアウトの実装
link_toメソッドを使って、ホーム画面からログアウトできるリンクを作成します。
app/views/homes/index.html.erb<h3>home</h3> <div> <%= link_to 'logout', destroy_user_session_path, method: :delete %> </div>
destroy_user_session_path
は、ターミナルのrails routes
で確認できます。
devise/sessions#destroy
(sign_out)のPrefixが
destroy_user_session
になっているのが確認できると思います。
Prefixの後に_path
をつけて記述します。続いて、
method
をdelete
として完成です。logoutをクリックすると、ログイン画面に遷移しますので、これで完成です。
以上です。お疲れ様でした。
- 投稿日:2020-09-14T09:32:15+09:00
[Rails]本番環境でエラー画面を表示させる方法
エラー画面とは
Railsのローカル環境ではエラーが発生した場合に下記画像のようにエラー画面が表示されるようになっており、
エラー発生箇所の発見を容易にしてくれている。
ただ、本番環境ではデフォルトでエラー画面が表示されずに下記画像のような画面が表示されるようになっている。
今回は本番環境でもローカル環境と同様にエラー画面が表示されるように設定する方法を紹介したいと思います!
設定変更方法
設定方法は簡単でアプリケーションの
config/environments/production.rb
にあるconfig.consider_all_requests_local = false
をtrue
に変更してあげるだけで設定できます!config/environments/production.rbRails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false #ここをtrueに変更 config.action_controller.perform_caching = true #以下省略 end最後に
これで本番環境でエラーが発生した場合に原因を特定しやすくなりましたね。
よければ参考にしてみて下さい。
- 投稿日:2020-09-14T09:08:23+09:00
Rails 6で認証認可入り掲示板APIを構築する #9 serializer導入
←Rails 6で認証認可入り掲示板APIを構築する #8 seed実装
ActiveModelSerializerの導入
serializerを入れることで、jsonで返されるデータを簡単に整形できます。
Gemfile... + # serializer + gem "active_model_serializers"$ bundle設定ファイルとserializerの編集
導入できたらpostモデルのserializerと、ActiveModelSerializerの設定ファイルも作ります。
$ rails g serializer post $ touch config/initializers/active_model_serializer.rbapp/serializers/post_serializer.rb# frozen_string_literal: true # # post serializer # class PostSerializer < ActiveModel::Serializer attributes :id endconfig/initializers/active_model_serializer.rb# frozen_string_literal: true ActiveModelSerializers.config.adapter = :jsonapp/controllers/v1/posts_controller.rbdef index posts = Post.order(created_at: :desc).limit(20) - render json: { posts: posts } + render json: posts end def show - render json: { post: @post } + render json: @post end def create post = Post.new(post_params) if post.save - render json: { post: post } + render json: post else render json: { errors: post.errors } end @@ -27,7 +27,7 @@ module V1 def update if @post.update(post_params) - render json: { post: @post } + render json: @post else render json: { errors: @post.errors } end @@ -35,7 +35,7 @@ module V1 def destroy @post.destroy - render json: { post: @post } + render json: @post end一旦ここまでやったら
rails s
を止めて、再起動しましょう。curlで確認
$ curl localhost:8080/v1/posts {"posts":[{"id":20},{"id":19},{"id":18},{"id":17},{"id":16},{"id":15},{"id":14},{"id":13},{"id":12},{"id":11},{"id":10},{"id":9},{"id":8},{"id":7},{"id":6},{"id":5},{"id":4},{"id":3},{"id":2},{"id":1}]} $ curl localhost:8080/v1/posts/1 {"post":{"id":1}}serializerでidのみにしているので、idの一覧が取得できました。
それではsubject, bodyを追加してみます。app/serializers/post_serializer.rb# frozen_string_literal: true # # post serializer # class PostSerializer < ActiveModel::Serializer - attributes :id + attributes :id, :subject, :body end$ curl localhost:8080/v1/posts {"posts":[{"id":20,"subject":"無駄","body":"ハチのすさいぼうかっこう。暴力血液恨み。秘めるちゅうもんする廃墟。"},... curl localhost:8080/v1/posts/1 {"post":{"id":1,"subject":"hello","body":"警官総括大尉。めいしぼきんかたみち。伝統徳川超〜。正常に動いていそうですね。
rubocopとrspecも動かして、問題なければcommitしておきましょう。続き
- 投稿日:2020-09-14T01:46:01+09:00
Railsで.scssをコントローラごとに分ける方法
アプリケーションの規模が大きくなるに連れてapplication.scssファイルの記述が長くなってきたので、今回コントローラーごとにファイルを分けることにしました。
ちょっと苦戦したり、勉強になったことが多かったので、忘れないために記事にすることにしました。環境
Ruby 2.5.7
Rails 5.2.4答え
先に答えだけ書いておきます。
application.scss// require_tree . // require_selfapplication.html.erb<head> ... <%= stylesheet_link_tag 'application' %> ... </head>この記述と各コントローラー名の.scssファイルをapp/assets/stylesheet/の中に作れば大丈夫です!
ここから少し中身を解説していきます。
経緯
//require_tree . って必要?
最初は、application.scssを共通のスタイルファイルとし、各ページのコントローラー名.scssの2ファイルで全てのページに対応しようとしました。
その時の記述は下記のようになります。application.scss// require_self
application.html.erb<head> ... <%# application.scssの読み込み %> <%= stylesheet_link_tag 'application' %> <%# コントローラー名.scssの読み込み %> <%= stylesheet_link_tag params[:controller] %> ... </head>application.scssファイルの
// require_tree .
はapp/assets/stylesheet内の全ての.scssファイルを読み込む記述なので、一旦削除します。
その代わりに、application.html.erbのstylesheet_link_tagでparams[:controller]と書くと、ディレクトリを含めたコントローラーのパスを取得できるので、コントローラーをディレクトリ分けしている場合もこの書き方で対応できます。しかし、実はこのやり方は.cssファイルならこれで大丈夫なのですが、.scssではエラーになってしまいます。
プリコンパイルで失敗する
.scssは.css形式にコンパイルすることで初めてブラウザに対応できます。
つまり、通常は.scss形式のままでは表示ができないということです。そして、プリコンパイルされた.scssファイル(.cssファイル)は本番環境ではapp/public/assets下に格納され、ファイル名はハッシュ形式に変更されてしまいます。
つまり、application.html.erbに記述した<%= stylesheet_link_tag params[:controller] %>
ではcssファイルが拾えなくなるということになります。
ハッシュ形式に変更されたファイル名でも、そのハッシュをそのままstylesheet_link_tag
の中で指定したら動作はしますが、このハッシュはファイルが更新されるごとに変更されてしまうので現実的ではありません。私は当初やりたかった.scssファイルを真にコントローラーごとに分割するというやり方にたどり着くことはできなかったので、冒頭の記述に切り替えました。
// require_tree . の有無の違いについて
application.scssに
// require_tree .
がある場合はassets/stylesheet下にある全ての.scssファイルが読み込まれるということになります。これはあまりあってはならない事なのですが、仮にcssセレクタのクラス名が被った状態でスタイル指定をすると、後に記述している方が優先されてしまうため、思い通り変更ができないなどの、思わぬところで依存関係ができてしまう恐れがあります。
.css(.scss)ファイル読み込みの順番
application.scss// require_tree . // require_self冒頭のように記述した場合は、全ての.scssファイルが読み込まれた後にapplication.scssが読み込まれます。
また// require_tree .
の中身の順番については辞書順となっており、ファイル名a→zの順番で読み込まれます。
必然的にtree .の中ではファイル名のイニシャルがzに近いほど後から読み込まれるため、優先度が高くなる傾向にあります。変数用に用意した.scssファイルの扱い
その前に.scssの変数について少しだけ確認しておきます。
scssではプロパティや値を変数にして使い回すことができます。
私の場合は今のところ
*ハンバーガーメニューのtransition
*サイト全体のカラーリング3パターンほど
*メディアクエリのwidth
をそれぞれ変数化して一つのファイルにまとめ、各ファイルでそのファイルを呼び出す記述をしています。
参考までに記載しておきます。_variables.scss// ハンバーガーアニメーション $hamburger-transition: 0.3s; // テーマカラー $thema-color1: #fff9f9; $thema-color2: #ffefef; $thema-color-font: #555; // メディアクエリ $media-sp-max: 450px; $media-pc-min: 1024px; $media-tb-min: $media-sp-max + 1px; $media-tb-max: $media-pc-min - 1px; //例 セレクタ名 { background: $thema-color1; }これでテーマカラーの変更や、メディアクエリのwidth、ハンバーガーメニューのtransitionなどを各ファイル一括で変更できるようにしています。
変数の定義は$から変数名を書き始め、その後に値を入れることで、定義できます。
呼び出すときはその変数名を値のところにそのまま書くだけです。しかし、このままでは各ファイルで変数が定義されていないので、この変数のみを記載したファイルを各コントローラー名の.scssファイルにインポートする必要があります。
コントローラー名.scss@import "variables"; ...
@import "ファイル名"
を記述することで、今回の場合だと、_variablesに書かれた変数が使用できるようになります。ここで疑問が生まれました。
「application.scssで// require_tree .
を記載している。各.scssファイルには変数ファイルのインポートを記載していているので、コンパイルの時に各ファイルが読み込まれるたびに変数ファイルも都度都度.cssファイルとして読み込まれるのでは?」と。
読み込まれたとしても問題ないと言えば無いのですが、やっぱり無駄が多いと思ったので、さらに調べました。結果から言うと、知らず知らずのうちにそれを回避していました。笑
共通ファイルをコンパイルから除外する"partial"
どこかの記事をみて変数ファイルの作り方を参考にファイル名を作ったのですが、そのファイル名の先頭に"_"アンダースコアをつけることで、コンパイルはされなくなるようです笑
つまり、今回使っている_variables.scssはアンダースコアから始まるファイル名のため、プリコンパイルからは除外されます。
よく考えてみると確かに変数しか書いていないファイルは直接スタイリングをしないので.cssに変換しても何も意味がないですし、他のファイルを.cssに変換する時に変数部分を中身に置き換えることが出来れば変数ファイルはそれだけ用が済む話だなと、変に納得しました笑まとめ
ここで冒頭の実装方法に戻りますが、とりあえずはこのやり方で運用してみようかと思っています。
application.scss// require_tree . // require_self ..._variables.scss$変数名: 値; ...各コントローラー名.scss@import "variables"; ...application.html.erb<head> ... <%= stylesheet_link_tag 'application' %> ... </head>これまではapplication.scssにしかスタイルを書いておらず、コメントを駆使してコントローラー名を書いてブロックを作ったりしていましたが、コントローラーごとにファイルが分けられるだけでもメンテナンスがやりやすくなるかなと思っています。
もし万が一どうしてもクラス名が被ってしまう場合については.scssの特徴でもあるセレクタのネストを用いて視覚的にもわかりやすく記述していきます。(使い回し用のclass名(flexやgrid、btnなど)は別途application.scssファイルに記述しています。)
// require_tree .
を使わずに各コントローラーごとの.scssファイルをプリコンパイルする方法があればご教授いただけると幸いです!
また、質問や解釈の違い、記述方法にも違和感などありましたら、コメント等でご指摘いただけると幸いです。最後まで読んでいただきありがとうございました!
参考サイト
Railsガイド - アセットパイプライン
Web Design Leaves - SASS
CSS HappyLife - Sassを覚えよう!Vol.7】ファイルを分割して管理を楽に(partialについて)
HACK NOTE - Sass:変数を別ファイルで管理しよう
- 投稿日:2020-09-14T01:02:55+09:00
�市のプレミアム商品券の案内があんまりだったので、勝手にLINEBotで使いやすくしてみた
はじめに
私は埼玉県川口市に住んでいるのだが、最近「プレミアム付き商品券」を発行するとのチラシをもらった。
ざっくり説明すると、2万円で商品券を買うと、地元のお店で2万4千円分のお買い物ができるようだ。これはなかなか便利だと思い、どんなお店で使えるのか調べようとホームページへアクセスしたが、、、なんとお店一覧が存在せず、お店の情報をまとめたPDFへの直リンクがおいてあるのみだった。。。
これでは検索も大変だ。。。
ということで、勝手にLINEBot化し、
・キーワードでの検索
・位置情報から最寄りの使えるお店を検索
ができるように実装をしてみた。この記事で説明すること
Railsアプリケーションの作成
Herokuへのデプロイ
LIENBotの準備
LIENBotの実装(オウム返しBOT)
CSVファイルの読み込み
簡単な検索機能の実装詳細な実装は後編にわけます。
環境
Ruby 2.6.6
Rails 6.0.3.3事前準備
rbenvのインストール
gitのインストール
herokuのアカウント登録
PDFをCSVに変換できるなんらかのツール(私はAdobe Acrobatでやりました)Railsアプリケーションの作成
まずはアプリ用のディレクトリを作成します(この記事のやり方だとそのままアプリケーション名になるので考えてから作りましょう)
$ mkdir kawaguchi_ticketl_inebot $ cd kawaguchi_ticketl_inebotruby のバージョンはよほど古くなければなんでもいいと思いますが、ここではとりあえず2.6.6を指定してみます
$ rbenv install 2.6.6 $ rbenv local 2.6.6bundle init を実行しGemfileを作成しましょう
$ bundle init作成されたGemfileのRailsのコメントアウトを削除し、bundle install
$ bundle install --path=vendor/bundle用途がLINEBotだけなので、apiモードでRailsアプリケーションを作成します。
herokuにスムーズにあげる関係で、postgreqlで作っておきます。
Gemfileの上書きをするか尋ねられると思いますが、上書きしちゃって大丈夫です。$ bundle exec rails new . --api -d postgresql $ bundle exec rails db:createここまでできたらサーバーを起動し、アクセスできるかだけ確認します
http://localhost:3000/ にアクセスして確認$ bundle exec rails sできてますね
(参考)
こちらの記事が大変わかりやすかったです
初心者がRubyで自作したLINE botを公開するまで
rbenvでrubyのバージョンを管理するHerokuへのデプロイ
後からでもいいですが、いったんherokuへpushしておきます。
herokubへの登録や設定がまだでしたら先にそちらを済ませておいてください$ heroku create Creating app... done, ⬢ young-temple-xxxxxx https://young-temple-xxxxxx.herokuapp.com/ | https://git.heroku.com/young-temple-xxxxxx.git $ git add . $ git commit -m 'first commit' $ git push heroku masterheroku create したときに表示されるURLはあとで使うのでメモっておきます
(ここでいう、 https://young-temple-xxxxxx.herokuapp.com/ )githubなどにあげてもいいですが、とりあえずスキップします
(参考)
github にpushしてherokuにあげるまでの流れLINEBotの準備
チャネルの登録
こちらを参考に
https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-consoleBotの登録
こちらを参考に
https://developers.line.biz/ja/docs/messaging-api/building-bot/Webhook URLはまだ設定できないので、このあとで設定をします。
チャネルアクセストークンとチャンネルシークレットをherokuの環境変数に設定します
$ heroku config:set LINE_CHANNEL_SECRET=xxxxxxxxxxxxxxxxxx $ heroku config:set LINE_CHANNEL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxLINEBotの実装
Gemfileに以下を追加
gem 'line-bot-api'$ bundle installroutes追加
routes.rbRails.application.routes.draw do # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html post '/callback' => 'webhook#callback' endコントローラーを作成
$ rails g controller webhookまずは https://github.com/line/line-bot-sdk-ruby のサンプル通りに作ってみましょう
app/controllers/webhook_controller.rbclass WebhookController < ApplicationController require 'line/bot' def client @client ||= Line::Bot::Client.new { |config| config.channel_secret = ENV["LINE_CHANNEL_SECRET"] config.channel_token = ENV["LINE_CHANNEL_TOKEN"] } end def callback body = request.body.read signature = request.env['HTTP_X_LINE_SIGNATURE'] unless client.validate_signature(body, signature) error 400 do 'Bad Request' end end events = client.parse_events_from(body) events.each do |event| case event when Line::Bot::Event::Message case event.type when Line::Bot::Event::MessageType::Text message = { type: 'text', text: event.message['text'] } client.reply_message(event['replyToken'], message) when Line::Bot::Event::MessageType::Image, Line::Bot::Event::MessageType::Video response = client.get_message_content(event.message['id']) tf = Tempfile.open("content") tf.write(response.body) end end # Don't forget to return a successful response "OK" end end endherokuにあげます
$ git add . $ git commit -m 'add controller' $ git push heroku masterこちらを参考にheroku createを行った時に表示されたURLをWebhook URLに設定します
https://developers.line.biz/ja/docs/messaging-api/building-bot/CSVファイルの読み込み
ここまでで作ったBotはただのオウム返しBotなので、データを読み込み、検索ができるようにします。
今回はこういったCSVを読み込みます
ここから入手したPDFをCSVにしたもの読み込むCSV用のモデルを作成します
$ bundle exec rails g model store Running via Spring preloader in process 76501 invoke active_record create db/migrate/20200911182431_create_stores.rb create app/models/store.rb invoke test_unit create test/models/store_test.rb create test/fixtures/stores.ymlマイグレーションファイルを編集
db/migrate/20200911182431_create_stores.rbclass CreateStores < ActiveRecord::Migration[6.0] def change create_table :stores do |t| t.string :store_association_name, comment: '商店会名' t.string :store_name, comment: '店舗名' t.string :postal_code, comment: '郵便番号' t.string :address, comment: '住所' t.string :tel, comment: '電話' t.string :lineup, comment: '取扱商品名' t.timestamps end end endマイグレーションを実行
$ bundle exec rake db:migratここまででデータを入れるところはできたので、CSVを取り込むプログラムを作成します
$ bundle exec rails g task import_csv Running via Spring preloader in process 76763 create lib/tasks/import_csv.rakelib/tasks/import_csv.rakerequire 'csv' namespace :import_csv do desc '川口市商店街の発行しているPDFをCSVにしたものを取り込み' task :store, ['file_name'] => :environment do |_, args| # インポートするファイルのパスを取得。 # ファイル名は複数ありそうなのでタスク実行時にファイル名を指定 path = Rails.root.to_s + '/db/csv/' + args.file_name # インポートするデータを格納するための配列 list = [] CSV.foreach(path, headers: true) do |row| list << { # 取り込むCSVのヘッダーにあわせて調整してください store_association_name: row['商店会名'], store_name: row['店舗名'], postal_code: row['郵便番号'], address: row['住所'], tel: row['電話'], lineup: row['取扱商品名'] } end puts 'インポート処理を開始' begin Store.create!(list) puts 'インポート完了' rescue => exception puts 'インポート失敗' puts exception end end endタスクが登録されているか確認
$ bundle exec rake -T rake about # List versions of all Rails frameworks and the environment rake action_mailbox:ingress:exim # Relay an inbound email from Exim to Action Mailbox (URL and INGRESS_PASSWO... rake action_mailbox:ingress:postfix # Relay an inbound email from Postfix to Action Mailbox (URL and INGRESS_PAS... rake action_mailbox:ingress:qmail # Relay an inbound email from Qmail to Action Mailbox (URL and INGRESS_PASSW... rake action_mailbox:install # Copy over the migration rake action_text:install # Copy over the migration, stylesheet, and JavaScript files rake active_storage:install # Copy over the migration needed to the application rake app:template # Applies the template supplied by LOCATION=(/path/to/template) or URL rake app:update # Update configs and some other initially generated files (or use just updat... rake db:create # Creates the database from DATABASE_URL or config/database.yml for the curr... rake db:drop # Drops the database from DATABASE_URL or config/database.yml for the curren... rake db:environment:set # Set the environment value for the database rake db:fixtures:load # Loads fixtures into the current environment's database rake db:migrate # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog) rake db:migrate:status # Display status of migrations rake db:prepare # Runs setup if database does not exist, or runs migrations if it does rake db:rollback # Rolls the schema back to the previous version (specify steps w/ STEP=n) rake db:schema:cache:clear # Clears a db/schema_cache.yml file rake db:schema:cache:dump # Creates a db/schema_cache.yml file rake db:schema:dump # Creates a db/schema.rb file that is portable against any DB supported by A... rake db:schema:load # Loads a schema.rb file into the database rake db:seed # Loads the seed data from db/seeds.rb rake db:seed:replant # Truncates tables of each database for current environment and loads the seeds rake db:setup # Creates the database, loads the schema, and initializes with the seed data... rake db:structure:dump # Dumps the database structure to db/structure.sql rake db:structure:load # Recreates the databases from the structure.sql file rake db:version # Retrieves the current schema version number rake import_csv:store[file_name] # 川口市商店街の発行しているPDFをCSVにしたものを取り込み rake log:clear # Truncates all/specified *.log files in log/ to zero bytes (specify which l... rake middleware # Prints out your Rack middleware stack rake restart # Restart app by touching tmp/restart.txt rake secret # Generate a cryptographically secure secret key (this is typically used to ... rake stats # Report code statistics (KLOCs, etc) from the application or engine rake test # Runs all tests in test folder except system ones rake test:db # Run tests quickly, but also reset db rake test:system # Run system tests only rake time:zones[country_or_offset] # List all time zones, list by two-letter country code (`rails time:zones[US... rake tmp:clear # Clear cache, socket and screenshot files from tmp/ (narrow w/ tmp:cache:cl... rake tmp:create # Creates tmp directories for cache, sockets, and pids rake yarn:install # Install all JavaScript dependencies as specified via Yarn rake zeitwerk:check # Checks project structure for Zeitwerk compatibilityタスクが登録されているようです。
次に取り込むCSVファイルを設置します。
dbの下にcsvというディレクトリを作成し、そこにCSVファイルを置きます
準備するのが面倒でしたら、ここから取得してください
CSV取り込み用のrakeコマンドを実行します
$ bundle exec rake import_csv:store['kawaguchi.csv'] インポート処理を開始 インポート完了本当にデータが入っているか確認します
$ bundle exec rails c Running via Spring preloader in process 77369 Loading development environment (Rails 6.0.3.3) irb(main):001:0> Store.all Store Load (0.7ms) SELECT "stores".* FROM "stores" LIMIT $1 [["LIMIT", 11]] => #<ActiveRecord::Relation [#<Store id: 1, store_association_name: nil, store_name: "㈱EKオート", postal_code: "332-0025", address: "原町16-10", tel: "255-4980", lineup: "車検、鈑金、一般修理、新車、中古車販売", created_at: "2020-09-11 18:37:12", updated_at: "2020-09-11 18:37:12">, #<Store id: 2, store_association_name: nil, store_name: "ACE-LAB", postal_code: "332-0034", address: "並木3-3-19", tel: "287-9465", lineup: "美容室", created_at: "2020-09-11 18:37:12", updated_at: "2020-09-11 18:37:12">...入っているようです。
Storeに簡単な検索機能を追加
app/models/store.rbclass Store < ApplicationRecord def self.search(txt) Store.where(lineup: txt) .or(Store.where(store_association_name: txt)) .or(Store.where(store_name: txt)).limit(5) end def self.get_search_message(txt) stores = Store.search(txt) message = [] stores.each do |s| message << s.store_name end message << '検索結果がありませんでした' if message.blank? message.join(', ') end endapp/controllers/webhook_controller.rbclass WebhookController < ApplicationController require 'line/bot' def client @client ||= Line::Bot::Client.new { |config| config.channel_secret = ENV["LINE_CHANNEL_SECRET"] config.channel_token = ENV["LINE_CHANNEL_TOKEN"] } end def callback body = request.body.read signature = request.env['HTTP_X_LINE_SIGNATURE'] unless client.validate_signature(body, signature) error 400 do 'Bad Request' end end events = client.parse_events_from(body) events.each do |event| case event when Line::Bot::Event::Message case event.type when Line::Bot::Event::MessageType::Text message = { type: 'text', # ↓を修正 text: Store.get_search_message(event.message['text']) } client.reply_message(event['replyToken'], message) when Line::Bot::Event::MessageType::Image, Line::Bot::Event::MessageType::Video response = client.get_message_content(event.message['id']) tf = Tempfile.open("content") tf.write(response.body) end end # Don't forget to return a successful response "OK" end end end簡単すぎますが、詳細は後編で詰めるとしていったんherokuにあげましょう
$ git add . $ git commit -m 'easy search' $ git push heroku master自分の手元の環境で行ったことをherokuの環境でも行う必要があります。
$ heroku run rake db:migrate $ heroku run rake import_csv:store['kawaguchi.csv']
- 投稿日:2020-09-14T00:03:42+09:00
Docker環境でRails停止時にExit code 1が発生する
問題
Docker上でRails (Puma) を実行中に
docker stop
でコンテナを停止させようとするとExit 1 (SIGHUP) が発生します。
ECSやKubernetesを利用している場合、コンテナが正しく終了せず、予期しない問題を引き起こす可能性があります。% docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES xxx rails "bundle exec rails s…" 44 seconds ago Exited (1) 3 seconds ago api xxx原因
俗に言う「PID 1問題1」が原因。PID 1はinitプロセスと呼ばれるもので、システム起動時にカーネルによって呼び出される特別なプロセスです。initプロセスはシグナルハンドリングや子プロセスの生成、ゾンビプロセスの削除などを行います。
今回はRails (2)を起動した際にPID 1が使われてしまい、シグナルを正しくハンドリングできないといった問題が発生していました。# コンテナ上でtopコマンドを実行した結果 PID USER PR NI VIRT RES %CPU %MEM TIME+ S COMMAND 1 root 20 0 2.2m 1.5m 0.0 0.1 0:00.02 S /bin/bash /bin/docker-entrypoint.sh bundle exec rails s -b 0.0.0.プロセスを見ると、PID 1でRailsが起動していることが分かります。
対策
tini や dumb-init といったプログラムを使うことで、PID 1の子プロセスとしてアプリケーションを起動することが可能となります。
docker-compose 3.7以降が利用可能であれば、docker-compose.yml
にinit
パラメータを付けることで問題を回避することができます (3)。# Railsがinitプロセスの子プロセスとして起動する PID USER PR NI VIRT RES %CPU %MEM TIME+ S COMMAND 1 root 20 0 1.0m 0.0m 0.0 0.0 0:00.03 S /sbin/docker-init -- /bin/docker-entrypoint.sh bundle exec rails s -b 0.0.0.0 6 root 20 0 2.2m 1.5m 0.0 0.1 0:00.00 S `- /bin/bash /bin/docker-entrypoint.sh bundle exec rails s -b 0.0.0.0
docker-compose down
実行後にプロセスを見ると、143 (SIGTERM) でコンテナが停止しています。% docker-compose ps Name Command State Ports ----------------------------------------------------------------------------- api /bin/docker-entrypoint.sh ... Exit 143ECSを利用している場合
タスク定義に
initProcessEnabled: true
(4) を追加します。initProcessEnabled
はdocker run
の--init
に相当します。Kubernetesを利用している場合
tiniなどの軽量initを使う方法もありますが、Kubernetes 1.17からは Share Process Namespace (5) を使うことで問題を回避できるようです。