20200914のRailsに関する記事は17件です。

instagramのクローンアプリを作る②

はじめに

タイトルの通り、簡易版instagramのアプリを作っていきます。
下記の工程に分けて記事を執筆していきますので、順を追って読んでいただけたらなと思います。

アプリ作成〜ログイン機能の実装
写真投稿機能の実装 ←イマココ
③ユーザーページの実装
④いいね機能の実装
⑤投稿削除機能の実装

Active Storageの導入

Active Storageとは...
ファイルアップロードを行うための機能で、
これを使えばフォームで画像の投稿機能などが簡単に作れます。
※以下、アプリケーションのディレクトリで

ターミナル
rails active_storage:install

続けてphotoモデルを作成します。
photouserに紐づいているのでuser:belongs_to
caption:textとすることで、text型のカラムも作成

ターミナル
rails g model photo user:belongs_to caption:text

そして

ターミナル
rails db:migrate

最後にコントローラの作成を行います。

ターミナル
rails g controller photos

下準備完了です。

写真投稿ページへのリンクを作成

まず、ルーティングの設定を行います。

routes.rb
Rails.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_pathrails routesのPrefixで確認
Image from Gyazo

photosモデルにnew.html.erbを作成し、確認用に下記のように記述してみます。

app/views/photos/new.html.erb
<h3>写真投稿</h3>

下記のように、ホーム画面から写真投稿のページに遷移できていれば成功です。
Image from Gyazo

コントローラの設定

rails g controller photosで作成したコントローラに記述していきます。

photos_controller.rb
class 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.rb
class 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 # ←ここ
end
photo.rb
class 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をクリックで、
以下のように投稿できていればとりあえずはカタチになっているかと思います。
Image from Gyazo


以上です。お疲れ様でした。

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

《未経験→webエンジニア》実務1日目

この記事の目的

自分がやったこと、知らなかったこと、やるべきことを明確にし
1日あたりの成長速度を速める。

「エンジニアになって、実務始まるとこんな感じなんだー」という
参考にも!

【今日やったこと】

・オリエンテーション
・PCの環境設定
・入社後の書類もろもろ

【知らなかったこと】

・APIテストってなに?
https://qiita.com/k-penguin-sato/items/defdb828bd54729272ad

・AWSの資格について(知ってたけど、きちんと調べていなかった)
https://proengineer.internous.co.jp/content/columnfeature/13442

【明日】やるべきこと

・railsの環境構築
・テストのコードが読めるよう、手を動かす

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

ネストした 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の引数は配列として渡す
[(親)@インスタンス変数, (子)@インスタンス変数]の順で記述するようにする

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

Rails pry-byebugの使い方

Dockerを使用したRails開発におけるpry-byebugの使い方

自分への備忘録です。

手順

  1. Railsコンテナへのアタッチ
  2. ソースコードのデバッグ位置に"binding.pry"
  3. 任意のコード
  4. ソースコードの修正後、アタッチの解除

1.Railsコンテナへのアタッチ

Railsコンテナ名の確認

App $ docker ps
App $ docker attach *AppName

2. ソースコードのデバッグ位置に"binding.pry"

boards_controller.rb
class BoardsController < ApplicationController
  def index
  end

  def new
    @board = Board.new
    binding.pry
  end
end

3. 任意のコード

[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

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

【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?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.rb
append :linked_files, "public/foo.mp4"

ローカル環境などからcurrent(例:myproject/current/public/)に直接コピーしても、その場では動きますが、次のリリースを配備するときにディレクトリごと置き換わってしまうため、この一手間が必要です。

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

特定のページでJavaScriptを動作させる方法

動作環境
Ruby 2.6.5
Rails 6.0.3.2

基本的にJavaScriptは全てのページで発生しているため、JavaScriptを使用していないページでも検証ツールのconsole内でJavaScriptのエラーが発生してしまいます。それが、ようやく解決できたので、投稿してみました。

実装に必要なコード

hoge.js
if (location.pathname.match("hoge")){
  //ここからJavaScriptを書き始める。
}

これでhogeというパスでのみhoge.jsは動作します。

一応解説を入れておくと、location.pathnameにより現在のパスを取得しmatchでhogeと合っているのかを確認しています。

個人的にconsole内のJavaScriptのエラーが気になっていたので、これですっきりしました。

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

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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

任意の文字列が左から何文字目に出てくるかを出力したい

【概要】

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

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

RailsにDevise+OmniAuthでユーザ認証したい

今回やりたいこと

Deviseを使った基本的なユーザー認証機能

SNS認証には仮登録メールを介さずにワンクリック登録となるようにしたい

Deviseの初期設定でユーザー情報の編集をする際に逐一パスワードを求められるが、ユーザーフレンドリーではないのでパスワードを入力せずにユーザー情報を編集したい

この記事の個人的目的

備忘録。GitHubにこのプロジェクトのソースコードを残しておきます。

https://github.com/zizynonno/devise_omniauth

1 deviseの導入

1.1 プロジェクトの作成

新しいプロジェクトを作ります。

$ rails new devise_omniauth
$ cd devise_omniauth

1.2 Gemfileの追加とインストール

Gemfileに以下のgemを追加する。

Gemfile
source 'https://rubygems.org'

(省略)...

# Devise
gem 'devise'
gem 'devise-i18n'
gem 'omniauth-twitter'
gem 'omniauth-facebook'
gem 'dotenv-rails'

Gemfile
gem 'devise' #ユーザー認証
gem 'devise-i18n' #deviseのi18n
gem 'omniauth-twitter' #twitter認証
gem 'omniauth-facebook' #facebook認証
gem 'dotenv-rails' #環境変数の設定

gemをインストール。

$ bundle install

2 deviseの設定

devise関連ファイルを追加。

$ rails g devise:install

このコマンドを実行すると、ターミナルに英文でdeviseの設定について記載されています。
それでは1〜4まで実行していきましょう。

2.1 デフォルトURLの指定

config/environments/development.rb
Rails.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 }
end

2.2 root_urlの指定
1番で指定したhttp://localhost:3000/にアクセスした際に表示されるページを指定します。
このプロジェクトではページを1つも作っていないため、先に追加します。

Pagesコントローラーと、indexページとshowページを追加してみます。

$ rails g controller Pages index show

routes.rbに以下を指定します。

config/routes.rb
Rails.application.routes.draw do
  root 'pages#index'
  get 'pages/show'
  (省略)...
end

2.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.rb
class 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
end
app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end

3.2 マイグレーションファイル、Userモデルの編集

これを使うものだけコメントアウトしていきます。

db/migrate/20200912194315_devise_create_users.rb
class 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.rb
class 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]
end

3.3 omniauth用カラムの追加

ついでにomniauth-twitter,omniauth-facebookで使うprovideruidusernameをUserテーブルに追加します。

$ rails g migration add_columns_to_users provider uid username

以下のようなマイグレーションファイルができます。

db/migrate/20200912194427_add_columns_to_users.rb
class 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キー、シークレットキーを取得する

Facebook

以下よりアプリケーションを作成する。

作成が完了したら、設定より「Add Platform」→「Website」を選択する。
サイトURLにURLを入力する(例:http://localhost:3000)。

Twitter

以下よりアプリケーションを作成する。

作成が完了したら、「Settings」より以下の設定を行なう。

  1. Callback URL
    • 例:http://〜/users/auth/twitter
  2. 以下にチェックを入れる:
    • Allow this application to be used to Sign in with Twitter

4.2 設定ファイルの編集

それぞれのAPIキー、シークレットキーを以下の該当箇所にコピーして貼り付けます。

config/initializers/devise.rb
Devise.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にコミットしないでください
end

4.3 Userコントローラにコールバック処理を実装

providerと同じ名前のメソッドを定義する必要がある。
ただ、基本的に各プロバイダでのコールバック処理は共通しているので、callback_fromメソッドに統一している。

$ rails generate devise:controllers users
app/controllers/users/omniauth_callbacks_controller.rb
class 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
end

4.4 ルーティング処理

以下のように、OAuthのコールバック用のルーティングを設定する。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

  # ...
end

5 APIキー,シークレットキーの非公開

Twitter認証,Facebook認証をするためのそれぞれのAPIキー、シークレットキーを取得しました、この情報は非常に機密性が高く決して外部に漏らしてはいけない情報です。悪用される恐れがあるため、GitHubのリモートリポジトリや本番環境でAPIキー,シークレットキーが間違って一般公開されないような処理を施す必要があります。

最初の方で環境変数を設定するためのgem 'dotenv-rails'bundle installしているので、dotenv-railsを使ったAPIキー、シークレットキーの方法を学んでいきます。

5.1 .envファイルの設置
次に、環境変数を定義する.envファイルをアプリのプロジェクトルート直下に設置します。
.envファイルはdotenv-railsbundle installしても自動生成されない為、下記の様にtouchコマンドを使って手動でファイルを作成する必要があります。

$ touch .env 

5.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.rb
Devise.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']
end

6 ユーザーモデルにメソッドを追加する

Userモデルにself.from_omniauthself.new_with_sessionを作ります。
self.from_omniauthではuidとproviderで検索してあったらそれを、無かったらレコードを作ります。
self.new_with_sessionについては、もしこのメソッドを追加しておかなければ、Twitter認証後サインアップページで登録を行っても、認証情報として取ってきたuidやproviderなどが登録されません。それらが登録されないのでTwitterで認証しても登録されてないユーザーとして毎回サインアップページに飛ばされます。

app/models/user.rb
class 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
end

6.1 Userモデルにfindメソッドを実装

uidproviderの組み合わせは一意であり、これによりユーザを取得する。
レコードに存在しない場合は作成する。

app/models/user.rb
class 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での認証時もメールアドレスを保存する必要がある。
ここでは、uidproviderの組み合わせが一意なことを利用して、self.dummy_emailのように生成している。

以下ファイルを編集して、コールバック用のコントローラーとしてさっき作ったコントローラーが呼ばれるようにします。これを書かないとdevise側のコントローラーが呼ばれます。

config/routes.rb
Rails.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.rb
class 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

8 ユーザー情報の編集で逐一パスワードを求められるのがだるい

8.1 routes.rbを修正する

routes.rb
devise_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'
  (省略)...
end

8.2 update_resourceメソッドをオーバーライドする

registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController

  protected
  # 追記する
  def update_resource(resource, params)
    resource.update_without_password(params)
  end
end

8.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のドキュメント(英語)

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

instagramのクローンアプリを作る①

はじめに

タイトルの通り、簡易版instagramのアプリを作っていきます。
下記の工程に分けて記事を執筆していきますので、順を追って読んでいただけたらなと思います。

アプリ作成〜ログイン機能の実装 ←イマココ
写真投稿機能の実装
③ユーザーページの実装
④いいね機能の実装
⑤投稿削除機能の実装

まずはアプリケーションを作成

ターミナルを開いて下記コマンドを打ち込みます。
データベースはmysqlを使用していきますので、
オプションで「 -d mysql 」としています。

ターミナル
rails new instaclone -d mysql

作成できたらエディターを立ち上げて、「 datebase.yml 」を編集します。
encodingutf8に修正します。

datebase.yml
default: &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を行います。

Gemfile
gem 'devise'
ターミナル
instaclone % bundle install

gemの追加が完了したら、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.rb
class HomesController < ApplicationController
  def index
  end
end
routes.rb
Rails.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.rb
class HomesController < ApplicationController
  before_action :authenticate_user! #←ここ
  def index
  end
end

これで画面を更新すると下記のような画面に遷移するはずです。
Image from Gyazo

この画面で、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で確認できます。
Image from Gyazo

devise/sessions#destroy(sign_out)のPrefixが
destroy_user_sessionになっているのが確認できると思います。
Prefixの後に_pathをつけて記述します。

続いて、methoddeleteとして完成です。

このようになっていたら成功です。
Image from Gyazo

logoutをクリックすると、ログイン画面に遷移しますので、これで完成です。


以上です。お疲れ様でした。

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

[Rails]本番環境でエラー画面を表示させる方法

エラー画面とは

Railsのローカル環境ではエラーが発生した場合に下記画像のようにエラー画面が表示されるようになっており、
エラー発生箇所の発見を容易にしてくれている。
image.png

ただ、本番環境ではデフォルトでエラー画面が表示されずに下記画像のような画面が表示されるようになっている。
image.png

今回は本番環境でもローカル環境と同様にエラー画面が表示されるように設定する方法を紹介したいと思います!

設定変更方法

設定方法は簡単でアプリケーションのconfig/environments/production.rbにあるconfig.consider_all_requests_local = falsetrueに変更してあげるだけで設定できます!

config/environments/production.rb
Rails.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

最後に

これで本番環境でエラーが発生した場合に原因を特定しやすくなりましたね。
よければ参考にしてみて下さい。

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

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.rb
app/serializers/post_serializer.rb
# frozen_string_literal: true

#
# post serializer
#
class PostSerializer < ActiveModel::Serializer
  attributes :id
end
config/initializers/active_model_serializer.rb
# frozen_string_literal: true

ActiveModelSerializers.config.adapter = :json
app/controllers/v1/posts_controller.rb
     def 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しておきましょう。

続き

Rails 6で認証認可入り掲示板APIを構築する #10 devise_token_auth導入
連載目次へ

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

Railsで.scssをコントローラごとに分ける方法

アプリケーションの規模が大きくなるに連れてapplication.scssファイルの記述が長くなってきたので、今回コントローラーごとにファイルを分けることにしました。
ちょっと苦戦したり、勉強になったことが多かったので、忘れないために記事にすることにしました。

環境

Ruby 2.5.7
Rails 5.2.4

答え

先に答えだけ書いておきます。

application.scss
// require_tree .
// require_self
application.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:変数を別ファイルで管理しよう

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

�市のプレミアム商品券の案内があんまりだったので、勝手にLINEBotで使いやすくしてみた

はじめに

私は埼玉県川口市に住んでいるのだが、最近「プレミアム付き商品券」を発行するとのチラシをもらった。
ざっくり説明すると、2万円で商品券を買うと、地元のお店で2万4千円分のお買い物ができるようだ。これはなかなか便利だと思い、どんなお店で使えるのか調べようとホームページへアクセスしたが、、、なんとお店一覧が存在せず、お店の情報をまとめたPDFへの直リンクがおいてあるのみだった。。。
スクリーンショット 2020-09-13 23.44.02.png

これでは検索も大変だ。。。
ということで、勝手に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_inebot

ruby のバージョンはよほど古くなければなんでもいいと思いますが、ここではとりあえず2.6.6を指定してみます

$ rbenv install 2.6.6
$ rbenv local 2.6.6

bundle 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

スクリーンショット 2020-09-13 23.59.14.png

できてますね

(参考)
こちらの記事が大変わかりやすかったです
初心者が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 master

heroku create したときに表示されるURLはあとで使うのでメモっておきます
(ここでいう、 https://young-temple-xxxxxx.herokuapp.com/

githubなどにあげてもいいですが、とりあえずスキップします

(参考)
github にpushしてherokuにあげるまでの流れ

LINEBotの準備

チャネルの登録

こちらを参考に
https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-console

Botの登録

こちらを参考に
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=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

LINEBotの実装

Gemfileに以下を追加

gem 'line-bot-api'
$ bundle install

routes追加

routes.rb
Rails.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.rb
class 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
end

herokuにあげます

$ 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/

スクリーンショット 2020-09-14 0.57.13.png

ここまでで「オウム返しBot」が完成
スクリーンショット 2020-09-14 0.22.25.png

CSVファイルの読み込み

ここまでで作ったBotはただのオウム返しBotなので、データを読み込み、検索ができるようにします。

今回はこういったCSVを読み込みます
スクリーンショット 2020-09-14 0.24.47.png
ここから入手した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.rb
class 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.rake
lib/tasks/import_csv.rake
require '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ファイルを置きます
準備するのが面倒でしたら、ここから取得してください
スクリーンショット 2020-09-14 0.37.04.png

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.rb
class 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
end
app/controllers/webhook_controller.rb
class 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']

これで検索結果を返すBotになりました
スクリーンショット 2020-09-14 0.48.39.png

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

raila チュートリアル

5.3まで終了
5.4から

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

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が起動していることが分かります。

対策

tinidumb-init といったプログラムを使うことで、PID 1の子プロセスとしてアプリケーションを起動することが可能となります。
docker-compose 3.7以降が利用可能であれば、docker-compose.ymlinit パラメータを付けることで問題を回避することができます (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 143

ECSを利用している場合

タスク定義に initProcessEnabled: true (4) を追加します。initProcessEnableddocker run--init に相当します。

Kubernetesを利用している場合

tiniなどの軽量initを使う方法もありますが、Kubernetes 1.17からは Share Process Namespace (5) を使うことで問題を回避できるようです。

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