20200909のRubyに関する記事は30件です。

【Rails】副問い合わせをする場合の書き方

副問い合わせ(入れ子)でSQLを流したい

重複したデータを確認する例で確かめる。

副問い合わせの形を作る

usersテーブル

id name address
1 sasaki tokyo
2 itou fukuoka
3 fujita nagasaki
4 sasaki osaka
5 itou nagano

例えば上のようなテーブルがあったとする。
nameが重複するレコードを取り出し、addressを確認したい。

sql1:nameが重複しているものを選択
SELECT name FROM users GROUP BY name HAVING  count(name) > 1

結果

name
itou
sasaki

これだとそれぞれのaddressがわからないため、この結果を踏まえて次のようにSQLを実行する。

sql2:重複結果を踏まえて
SELECT * FROM users WHERE name IN ('itou', 'sasaki') ORDER BY name

結果

id name address
2 itou fukuoka
5 itou nagano
1 sasaki tokyo
4 sasaki osaka

上記のようになりこれでそれぞれのaddressも取得できる。

ということでsql1とsql2を合わせて副問い合わせの形にする。

sql3:副問い合わせ(sql1,sql2)
SELECT * FROM users WHERE name IN (
  SELECT name FROM users GROUP BY name HAVING  count(name) > 1
  ) 
ORDER BY name

Railsで実装

結論

以下のように書くことができる

任意のコントローラ
duplicates = User.select(:name).group(:name).having("count(name) > 1")
@users = User.where(name: duplicates)

実行されるSQL

Railsにて実行されるSQL
 SELECT "users".* FROM "users" WHERE "users"."name" IN (
   SELECT "users"."name" FROM "users" GROUP BY "users"."name" 
   HAVING (count(name) > 1)
 )

びっくりすることに発行されるSQLは1回になっている。

参考(かなり解りやすいです!)
ActiveRecord の IN 演算子でサブクエリを扱う(Oakbow様)

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

記事を複数使うのは要注意

参考記事の数

 Qiitaには魅力的な記事が数多くあるため、多くの記事を参考にしがちである。しかし、記事によって書き方は様々であるため、その内容を理解しないまま複数の記事を参考にすると、エラーの原因となりがちである。
そのため、一つの機能を実装する際には、一つの記事のみを参考にすべきである。

複数の記事を参照すると・・・

たとえば、DM機能を作りたいとして
https://qiita.com/tenitiumai/items/3d9466d7a24197f690bb 
を参考にしたとしよう。

model/message.rb
class Message < ApplicationRecord
  belongs_to :user
  belongs_to :room
end

上記の記事のuser.rbにはこのようなコードが記述されていたとしよう。
しかし、別の記事も参考にしたために、実際には

model/message.rb
class User < ApplicationRecord
  validate :messages, presence: true
  has_many :messages, dependent: :destroy
  has_many :entries, dependent: :destroy
end

と記述されていたとしよう。

 これ一つでは何ともなくても、複数の不要なコードを書き続けていると、予期せぬエラーの原因となる。なお、ここではmessagesをvalidateで縛る必要はない。なぜなら、messageのモデルなのに、必ずmessageを要求するというコードを書くのはおかしいからである。

models/user.rb
  has_many :comments, dependent: :destroy 
  has_many :entries, dependent: :destroy
  has_many :messages, dependent: :destroy
  has_many :rooms, dependent: :destroy

これも不要な部分がある。DM機能において、userは中間テーブルであるentriesを経由してroomに到達するため、has_many :roomsと書く必要はない。
上記のように、不必要がコードが積み重なると、スクリーンショット 2020-09-09 14.20.39.png
このようなエラーが出ることになる。特に、model部分はcontrollerやacitonを作成する際の基盤となるので、modelページで不必要なコードを書くのはできるだけ控えた方が無難である。

まとめ

 今回のエラーは基本をきちんと理解していないために引き起こされたエラーである。初心者のうちはやりがちであるので、記事を参考にする際はできるだけ一つに留めておくのが無難である(基本が理解できるのであれば複数を参照しても問題ないが・・・)。

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

ABC177 - EをRubyで解く

はじめに

AtCoder Beginner Contest 177E問題について細かく解説を書く機会があり、思いのほか出来が良かったので記事としてまとめようと思う。

考察

問題より、焦点を以下の順番で三つに分割できる。

  1. 『全ての1 ≦ i < j ≦ N について、GCD(A_i, A_j) = 1』が成り立つ
  2. 『GCD(A_1 …… A_N) = 1』が成り立つ
  3. いずれも成り立たない

次より、これら三つをより細かく見ていく。

1.『全ての1 ≦ i < j ≦ N について、GCD(A_i, A_j) = 1』が成り立つ

全ての場合を愚直に求める。

N.times do |i|
  N.times do |j|
    if A[i].gcd(A[j]) != 1 then
      is_pairwise_coprime = false
    end
  end
end

制約より、$2≤N≤10^6$ なので最大が $O(10^{12})$ となって実行時間制限2secに間に合わない。

ここでいずれの組み合わせの最大公約数が1となる点に注目する。
これはどの整数の組み合わせ $(A_i, A_j)$ も互いに素であると言い換えられる。

よって、N個の整数全てを素因数分解をして、同じ素因数が出てこなければこれが成り立ち、pairwise coprimeを出力する。

2.『GCD(A_1 …… A_N) = 1』が成り立つ

これは愚直にまわしてもO(N)なので、考察する余地はない。

res = a[0]
a.each do |elm|
  res = res.gcd(elm)
end

1が成り立たないとき、かつ、res == 1のとき、setwise coprimeを出力すればいい。
そして、3.いずれも成り立たないとき、not coprimeを出力する。

これより、1が考察の主となる。

素因数分解

Rubyにはprimeモジュールがあり、これのPrime.prime_divisionより素因数分解ができる。
参考:Rubyで素数で遊ぶ(prime モジュール)

これなら楽々解けそうだ。
提出!
image.png

間に合わない。ではどうするか。

osa_k法

エラトステネスのふるいと高速素因数分解を見てもらうのが一番早いのだが、ここでも自分なりに解説していく。

N(今回は $A_{max}$ )以下の正の整数を素因数分解するアルゴリズムであり、前処理($O(log log N)$)の後、ある数Mの素因数を求める本処理($O(log M)$)を行う。
計算量は $O(log log N + log M)$より、 $O(log M)$。
今回の最大値 $A_{max} = 10^6$ より $O(log 10^6) = O(10)$ 程度になる。
従って、N個全ての整数全ての素因数を求める計算量は $O(N log A_{max}) = O(10^7)$ 程度となり、十分間に合う。

要約すると、

  • 正の整数を素因数分解する
  • 最小の素因数を求める前処理を行う
  • 前処理した結果を用いて素因数分解を高速に行う

アルゴリズムである

前処理

$N = A_{max}$ 以下の整数(1~N)について、最小の素因数min_factorを求める。
エラトステネスの篩の応用で、i(= 2 ~ $\sqrt{N}$)の倍数kを順にみて、iがkに入っている値より小さければiを入れる。

例:
素因数分解したい値の最大値N=16を例に挙げる。
$\sqrt{16} = 4$なので、iの範囲は2~4となる。
image.png

iを順番に見て行って最小の素因数を格納した配列min_factorをつくる。

image.png

最後の部分が配列min_factorとなる。

本処理

以下のフローチャートを参考にされたい。

image.png

resultに素因数分解した結果が格納される。
同じ素数が複数格納されるので、素因数のみ欲しい場合はuniqsetを使う。

上記の例を以上のフローにあてはめると下図の通りになる。

image.png

これでACするコードを書けそうだ。

実装

class Osa_k
    # @parm {number} n - 素因数分解したい値の中での最大値
    def initialize(n)
        @min_factor = [*0..n]
        i = 2
        while i * i <= n do
            if @min_factor[i] == i then
                j = 2
                while i * j <= n do
                    @min_factor[i*j] = i if @min_factor[i*j] > i
                    j += 1
                end
            end
            i += 1
        end
    end

    # @parm {number} m - 素因数分解したい値
    # @return {array} res - mの素因数群
    def factor(m)
        res = []
        tmp = m
        while tmp > 1 do
            res << @min_factor[tmp]
            tmp /= @min_factor[tmp]
        end
        return res.uniq
    end
end

MAX = 10 ** 6 + 10 # 最大値が10^6なので
n = gets.chomp.to_i
a = gets.chomp.split.map(&:to_i)
osa_k = Osa_k.new(MAX)
res = a[0]
h = Hash.new(0) # 連想配列を用いて素因数が既出かどうかを管理
is_pairwise_coprime = true # 1つ目の条件を満たしているかどうか
n.times do |i|
    # 1つ目の条件が満たされないと分かった時点でやる必要はない
    if is_pairwise_coprime then
        osa_k.factor(a[i]).each do |num, cnt|
            if h.has_key?(num) then
                is_pairwise_coprime = false
                break
            end
            h[num] += 1
        end
    end
    res = res.gcd(a[i]) # 2つ目の条件を確認
end

if is_pairwise_coprime then
    puts "pairwise coprime"
elsif res == 1 then
    puts "setwise coprime"
else
    puts "not coprime"
end

提出結果:https://atcoder.jp/contests/abc177/submissions/16583524

おわりに

エラトステネスのふるいと高速素因数分解のおかげでAC出来た。ここに感謝の意を表したい。

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

Rubyアルゴリズム(inject,メソッド定義)

はじめに

Rubyアルゴリズムを学習する際に作成したコードを解説します。

学んだ事

次の技術に挑戦しました。

  • injectメソッド
  • メソッド定義

挑戦したこと

10個ずつ数字を並べた二つの配列を使用します。
それぞれの配列の、1番目・1番目から2番目の和・3番目までの和…を比較し、
どちらが大きいか表示するコードを作成します。

内容

まずはなんでもいいので、配列を二つ用意します。

a = [1,2,3,4,5,6,7,8,9,10]
b = [10,2,2,2,2,2,2,2,2,2]

次に
配列の和を求めるメソッドを作ります。
色々方法はありますが、ここではinjectメソッドを使用します。

def sum(array,n)
  sum = array[0,n].inject(:+)
  return sum
end

array[0,n].inject(:+)の部分では、引数として代入した配列の1番目からn+1番目の和を計算します。

最後に、上記の処理を配列の全ての要素に対して実行し、その度に大小を判定するようにします。

(1..10).each{|i|
  if sum(a,i) > sum(b,i)
    puts "#{i}:aが大きい"
  elsif sum(a,i) < sum(b,i)
    puts "#{i}:bが大きい"
  else
    puts "#{i}:等しい"
  end
}

全てを合わせると下記の通りになります。

a = [1,2,3,4,5,6,7,8,9,10]
b = [10,2,2,2,2,2,2,2,2,2]

def sum(array,n)
  ans = array[0,n].inject(:+)
  return ans
end

(1..10).each{|i|
  if sum(a,i) > sum(b,i)
    puts "#{i}:aが大きい"
  elsif sum(a,i) < sum(b,i)
    puts "#{i}:bが大きい"
  else
    puts "#{i}:等しい"
  end
}

以上です。

おわりに

今回のコードで新しくinjectメソッドの使い方を知ったほか、
メソッド返り値同士でも大小比較ができるという気付きが得られました。
今後も、新しい知識を得ていくと共に、既に知っている知識の新しい使い方を追求したいと思います。

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

Ractorで複数のオブジェクトを渡せるようにしてみた

結論

Ractorで以下のコードを動くようにしてみました。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send(1, 2)

r.take
# => 1
# => 2
# => Integer
# => Integer

Ractorって?

Ruby3で導入される並行・並列機能を提供するしくみです。
元々はGuildという名前で数年前から議論されてきたものです。

詳しい話は下記の動画を参照して頂ければと思います。

[JA] Ractor report / Koichi Sasada @ko1

Ractorへオブジェクトを渡す

sendメソッドを使ってRactorへとオブジェクトを渡すことができます。

r = Ractor.new do
    v = Ractor.recv
    puts v
    puts v.class
end

r.send(1)

r.take
# => 1
# => Integer

しかし、以下のように複数のオブジェクトを渡すことはできません。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send(1, 2)

r.take
# =>wrong number of arguments (given 2, expected 1) (ArgumentError)

ただし配列で渡す分にはOKみたいです。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send([1, 2])

r.take
# => 1
# => 2
# => Integer
# => Integer

実装を見てみると以下のようになっています(CRubyのソースコード内のractor.rbにて)
現在のsendメソッドは一つのオブジェクトのみを引数に受け取ります。またムーヴして良いかどうかをキーワード引数moveで指定することもできます。

  def send obj, move: false
    __builtin_cexpr! %q{
      ractor_send(ec, RACTOR_PTR(self), obj, move)
    }
  end

__builtin_cexpr!でCの関数を呼び出し、メソッドが受け取った引数をそのままCの関数に渡しています。余談ですが、最近のCRubyでは内部実装としてRubyの変数をCの関数に渡すようなコードを書くことができるようになっています。

やったこと

Ractorのsendメソッドを以下のように書き替えてみました。

  def send obj, *arg, move: false
    obj = arg.unshift obj unless arg.empty?
    __builtin_cexpr! %q{
      ractor_send(ec, RACTOR_PTR(self), obj, move)
    }
  end

まず、sendメソッドは必ず一つのオブジェクトを引数として受け取っています。その挙動を維持するためにobj, *arg, move: falseのように引数を書き換えています。
またsend(1, 2)のように複数オブジェクトが渡された場合は*argに配列として引数が渡されます。

argが空の配列でない場合、複数オブジェクトが渡されていることになり、最終的にCの関数に渡されるobjを第一引数と可変長引数をマージしたものへと変換しています。

あとは修正したCRubyのソースコードをビルドすればOKです。

これで以下のようにRactorに複数のオブジェクトを渡すことができます。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send(1, 2)

r.take
# => 1
# => 2
# => Integer
# => Integer

参考

ref: Guild → Ractor
ref: https://github.com/ko1/ruby/blob/ractor/ractor.ja.md
ref: [[JA Ractor report / Koichi Sasada @ko1

追記

ちなみに、モンキーパッチでよければ以下のようにラップしたメソッドを作成すればOKです

class Ractor
    def multi_send(obj, *args, move: true)
        obj = args.unshift obj unless args.empty?
        send(obj, move: move)
    end 
end

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.multi_send(1, 2)

r.take

モンキーパッチだと範囲が広いので実務とかで使うならrefinements使った方が影響が少ないのでいいかもしれない。

module RefineRactor
    refine Ractor do
        def multi_send(obj, *args, move: true)
            obj = args.unshift obj unless args.empty?
            send(obj, move: move)
        end 
    end
end

using RefineRactor

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.multi_send(1, 2)

r.take

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

DockerでRuby on rails6のアプリケーションを環境構築

目標

  • ruby on rails6 のアプリケーションをdockerで新規にアプリを立ち上げたい
  • DBはMySQLで設定

前提

  • Docker on mac
  • Rails tutorial完了などruby on rails に関する基礎知識

1.作業ディレクトを作成し、移動する

MacBook-Air ~ % mkdir アプリ名
MacBook-Air ~ % cd アプリ名
MacBook-Air アプリ名 %

2.Dockerfileを定義する

FROM ruby:2.6.3
RUN apt-get update -qq && apt-get install -y nodejs

# yarnパッケージ管理ツールをインストール
# https://classic.yarnpkg.com/en/docs/install/#debian-stable
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install yarn

WORKDIR /アプリ名
COPY Gemfile /アプリ名/Gemfile
COPY Gemfile.lock /アプリ名/Gemfile.lock
RUN bundle install
COPY . /アプリ名

# Add a script to be executed every time the container starts
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

3.Gemfileを作成する

source 'https://rubygems.org'
gem 'rails', '~>6'

4.空のGemfile.lockを生成する

MacBook-Air アプリ名 % touch Gemfile.lock

5.entrypoint.shを作成する

2つ目のところ、自身のアプリ名を入れることに気をつける

#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /アプリ名/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

6.docker-compose.ymlを作成する

webのvolumesのところを自分のアプリ名を入れる

docker-compose.yml
version: "3"

services:
  db:
    image: mysql:8.0
    command: mysqld --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: root
    volumes:
      - ./tmp/db:/var/lib/mysql
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    environment:
      MYSQL_HOST: db
    volumes:
      - .:/アプリ名
    ports:
      - "3000:3000"
    depends_on:
      - db

7.Rails newをする

ここではオプションとして、オプションでリンクしたサービスを起動しない設定、DBをmysqlにする設定を追加

MacBook-Air アプリ名 % docker-compose run web rails new . --force --no-deps --database=mysql
~
Starting アプリ名_db_1 ... done
Building web
~
Successfully built 36d2fef9a8a6
Successfully tagged アプリ名_web:latest

8. DBと接続する

passwordとhostに環境変数を設定する

database.yml
~
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: <%= ENV.fetch("MYSQL_ROOT_PASSWORD", "root") %>
  host: <%= ENV.fetch("MYSQL_HOST", "db") %>
~

9.コンテナを起動する

MacBook-Air アプリ名 % docker-compose up
~
web_1  | => Booting Puma
web_1  | => Rails 6.0.3.2 application starting in development 
web_1  | => Run `rails server --help` for more startup options
web_1  | Puma starting in single mode...
web_1  | * Version 3.12.6 (ruby 2.6.3-p62), codename: Llamas in Pajamas
web_1  | * Min threads: 5, max threads: 5
web_1  | * Environment: development
web_1  | * Listening on tcp://0.0.0.0:3000
web_1  | Use Ctrl-C to stop

この表示がでたらブラウザで http://localhost:3000/ にアクセスする
Yay! You’re on Rails! と表示がでたらコンテナ起動成功

エラー: No such file or directory @ rb_sysopen - /アプリ名/config/webpacker.yml Errno::ENOENT)発生時

MacBook-Air アプリ名 % docker-compose run web rails webpacker:install
~
Webpacker successfully installed ? ?

参考文献

https://docs.docker.com/compose/rails/
https://railsdoc.com/rails

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

DockerでRuby on rails6のアプリケーションを環境構築したメモ

目標

  • Ruby on Rails6 のアプリケーションをDockerで新規にアプリを立ち上げたい
  • DBはMySQL

前提知識

  • Docker on mac
  • Rails tutorial完了などRuby on rails に関する基礎知識

1.作業ディレクトを作成し、移動する

MacBook-Air ~ % mkdir アプリ名
MacBook-Air ~ % cd アプリ名
MacBook-Air アプリ名 %

2.Dockerfileを定義する

Dockerfile
FROM ruby:2.6.3
RUN apt-get update -qq && apt-get install -y nodejs

# yarnパッケージ管理ツールをインストール
# https://classic.yarnpkg.com/en/docs/install/#debian-stable
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install yarn

WORKDIR /アプリ名
COPY Gemfile /アプリ名/Gemfile
COPY Gemfile.lock /アプリ名/Gemfile.lock
RUN bundle install
COPY . /アプリ名

# Add a script to be executed every time the container starts
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

3.Gemfileを作成する

Gemfile
source 'https://rubygems.org'
gem 'rails', '~>6'

4.空のGemfile.lockを生成する

MacBook-Air アプリ名 % touch Gemfile.lock

5.entrypoint.shを作成する

2つ目のところ、自身のアプリ名を入れることに気をつける

entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /アプリ名/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

6.docker-compose.ymlを作成する

webのvolumesのところを自分のアプリ名を入れる

docker-compose.yml
version: "3"

services:
  db:
    image: mysql:8.0
    command: mysqld --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: root
    volumes:
      - ./tmp/db:/var/lib/mysql
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    environment:
      MYSQL_HOST: db
    volumes:
      - .:/アプリ名
    ports:
      - "3000:3000"
    depends_on:
      - db

7.Rails newをする

ここではオプションとして、オプションでリンクしたサービスを起動しない設定、DBをmysqlにする設定を追加

MacBook-Air アプリ名 % docker-compose run web rails new . --force --no-deps --database=mysql
~
Starting アプリ名_db_1 ... done
Building web
~
Successfully built 36d2fef9a8a6
Successfully tagged アプリ名_web:latest

8. DBと接続する

passwordとhostに環境変数を設定する

database.yml
~
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: <%= ENV.fetch("MYSQL_ROOT_PASSWORD", "root") %>
  host: <%= ENV.fetch("MYSQL_HOST", "db") %>
~

9.コンテナを起動する

MacBook-Air アプリ名 % docker-compose up
~
web_1  | => Booting Puma
web_1  | => Rails 6.0.3.2 application starting in development 
web_1  | => Run `rails server --help` for more startup options
web_1  | Puma starting in single mode...
web_1  | * Version 3.12.6 (ruby 2.6.3-p62), codename: Llamas in Pajamas
web_1  | * Min threads: 5, max threads: 5
web_1  | * Environment: development
web_1  | * Listening on tcp://0.0.0.0:3000
web_1  | Use Ctrl-C to stop

この表示がでたらブラウザで http://localhost:3000/ にアクセスする
Yay! You’re on Rails! と表示がでたらコンテナ起動成功

エラー: No such file or directory @ rb_sysopen - /アプリ名/config/webpacker.yml Errno::ENOENT)発生時

MacBook-Air アプリ名 % docker-compose run web rails webpacker:install
~
Webpacker successfully installed ? ?

参考文献

https://docs.docker.com/compose/rails/
https://railsdoc.com/rails

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

【Ruby on Rails】フォロー機能実装:双方向

目標

follow.gif

開発環境

ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina

前提

※ ▶◯◯ を選択すると、説明等が出てきますので、
  よくわからない場合の参考にしていただければと思います。

一方向のブックマーク(お気に入り登録、いいね)機能はこちら

流れ

1 modelを作成
2 modelを修正 <-- ここが一番むずかしいです
3 controllerを作成
4 routingを修正
5 viewを作成

modelの作成

今回はRelationshipモデルを作成

ターミナル
$ rails g model Relationship follower_id:integer followed_id:integer
ターミナル
$ rails db:migrate

補足
follower_idとfollowed_idは架空のidであり、後ほどuserモデルの修正にて記述し、使用します。

modelを修正

app/models/relationship.rb
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"

補足
メソッド名を変更し、@relationship.followerのような形で、@relationshipに紐づいたuserレコードを取得。
※メソッド名変更は、フォローとフォロワーを分けるためです。

app/models/user.rb
  has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy # フォロー取得
  has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy # フォロワー取得
  has_many :following_user, through: :follower, source: :followed # 自分がフォローしている人
  has_many :follower_user, through: :followed, source: :follower # 自分をフォローしている人

  # ユーザーをフォローする、後ほどcontrollerで使用します。
  def follow(user_id)
    follower.create(followed_id: user_id)
  end

  # ユーザーのフォローを外す、後ほどcontrollerで使用します。
  def unfollow(user_id)
    follower.find_by(followed_id: user_id).destroy
  end

  # フォローしていればtrueを返す、後ほどviewで使用します。
  def following?(user)
    following_user.include?(user)
  end

補足1【1,2行目について】
<has_many :follower>
 まず、userは多くのfollowerを持ち、
<class_name: "Relationship">
 @userに紐づいた@relationshipレコードを取得可能にし、
<foreign_key: "follower_id">
 relaitonshipsテーブルにアクセスする時、follow_idを入口とし、
<dependent: :destroy>
 userがなくなれば削除する

補足2【3,4行目について】
<has_many :following_user>
 まず、userは多くのfollowing_userを持ち、
<through: :follower>
 followerを通してfollowing_userを取得可能にし、
<source: :followed>
 user.following_userで取得可能にする。

補足3【(followed_id: user_id)について】
(followed_id: user_id)は
followed_id に user_idを代入する
という風にイメージするとわかりやすいです。
あとは文字通りの意味です。

controllerを作成

ターミナル
$ rails g controller relationships
app/controllers/relationships.controller.rb
  def create
    current_user.follow(params[:user_id])
    redirect_to request.referer
  end

  def destroy
    current_user.unfollow(params[:user_id])
    redirect_to request.referer
  end

今回はapp/views/homes/mypage.html.erbに
フォロー、フォロワーの一覧を表示するため下記の場所に記述。

app/controllers/homes.controller.rb
  def mypage
    @following_users = current_user.following_user
    @follower_users = current_user.follower_user
  end

routingを修正

config.routes.rb
resources :users do
  resource :relationships, only: [:create, :destroy]
end

viewに追加(インスタンス変数ありの場合)

app/views/show.html.erb
<% if current_user != user %>
  <% if current_user.following?(@user) %>
    <%= link_to 'フォロー外す', user_relationships_path(@user.id), method: :delete %>
  <% else %>
    <%= link_to 'フォローする', user_relationships_path(@user.id), method: :POST %>
  <% end %>
<% end %>
app/views/homes/mypage.html.erb
<div>
  フォロー数: <%= current_user.follower.count %>
  フォロワー数:<%= current_user.followed.count %>
</div><br><br>
<table>
    <caption>フォロー中</caption>
    <thead>
        <tr>
            <th>ユーザー名</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <% @following_users.each do |following_user| %>
        <tr>
            <td><%= following_user.name %></td>
            <td><%= link_to 'フォロー外す', user_relationships_path(following_user.id), method: :delete %></td>
        </tr>
        <% end %>
    </tbody>
</table><br><br>

<table>
    <caption>フォロワー</caption>
    <thead>
        <tr>
            <th>フォロワー名</th>
        </tr>
    </thead>
    <tbody>
        <% @follower_users.each do |follower_user| %>
        <tr>
            <td><%= follower_user.name %></td>
        </tr>
        <% end %>
    </tbody>
</table>

参考

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

deviseを使ったユーザー情報の変更をする場合パスワードが保存されない時のedit画面での設定

deviseを使ったユーザー登録情報の編集をする場合、edit→updateを行うときビュー画面のエラーメッセージがCurrent password can't be blankと表示され、編集画面が保存されない場合の解決方法。

ruby '2.6.5'
rails '6.0.0
devise 4.7.2

2つのdeviseをもつアカウントがあり、ユーザー登録情報を編集し、保存をしたい場合の設定の流れをまとめます。

①ルーティングの確認/rails routes でuserのregistrations/editのパスを確認します。
edit_user_registration GET    /users/edit(.:format)                                                          users/registrations#edit

edit_user_registration_path と確認が取れました。これをlink_toでパスを作成します。

②controllers/users/registrations_controller.rb ファイルです。

before_actionとconfigure_account_update_paramsの設定をします。今回は私のカラムを載せます。

before_action :configure_account_update_params, only: [:update]
def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update,  keys: [:nickname, :email, :password, :gender_id, :birth, :bloodtype_id, :emergencyperson, :emergencycall, :real_name, :real_name_kana, :phone_number])
  end
③次にビューです。

users/registrationsにedit.html.erbファイルを作成します。ほぼnew.html.erbをコピーしたものを使いますが、追加事項があります。私は2つのdeviseを使ってるのでディレクトリが違いますが1つのdeviseだとビューはdevise/registrationsにedit.html.erbを作成します。

form_withのパスも変更します。
参考サイト こちらです
結論から言うと、deviseのデフォルト設定なのですが、パスワード更新時は現在のパスワードを認証してupdateを行います。

<%= form_with model: @user, url: user_registration_path, method: :patch, class: 'registration-main', local: true do |f| %>
 <div class="field">
    <%= f.label :current_password %> 
    <%= f.password_field :current_password, autocomplete: "current-password" %>
  </div>

こちらを追加します。編集する場合は今までのパスワードを入れる項目が必要と言うわけです。

これを追加することによって無事に変更項目が保存されました。ですがパスワードを変更しない場合も必須なのでそこが?と言う感じです。
引き続き調べて学習していきます。

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

Rubocopの警告(error)を無視する方法

rescue => e  # rubocop:disable Style/RescueStandardError

こうやるともっと親切

rescue => e  # rubocop:disable Style/RescueStandardError why: -- rescue all to report Honeybadger
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アヤセ教えてくんについて

はじめに

「アヤセ教えて君」というLINE Botを開発しました。
まだまだ未完成な部分は多いですが、メインとなる機能の開発を終えたのでGitHub,Qiitaで公開することにしました。
この記事では、機能や技術的な解説をしていきます。
友達追加はこちら
友だち追加

GitHubリポジトリ

https://github.com/Aseiide/ayase_bot

アヤセ教えて君とは

demo.gif

  • 東京メトロ千代田線<下り方面>で活用できます
  • 千代田線において【現在いる駅】を送信すると、綾瀬行電車の時刻が取得できます

開発の経緯

千代田線.png

僕自身、大学・自宅ともに千代田線沿線にあります。
千代田線はJR常磐線に乗入れているため、下りは松戸・柏・取手などかなり遠いところまで繋がっているので夕方ラッシュではかなり混雑しています。
一方、綾瀬行・北綾瀬行の電車は行先が短く混雑している確率はとても低く、僕はこちらに乗るようにしていました。
Yahooの乗り換えアプリで、時刻表のページまで見に行くのが面倒だったので開発の経験を積む意味も含めてLINE Botの開発に取り組むことにしました。

技術解説

技術構成

ayasebot-sequence.png

使用しているAPI

LINE Messaging API

https://github.com/line/line-bot-sdk-ruby
オウム返しのBotを作った事があったのでSDKとドキュメントを参照しながら制作を進めました。

駅すぱあとWeb Service(フリープラン)

個人かつ無料で利用できるこちらのAPIを選定。
フリープランでは、単純に時刻を取得することができないのでAPIを叩いて返ってきたurlに対し、スクレイピングを行って時刻を取得しています。

コード解説

駅名を駅コードに変換

station_code = {
  :代々木上原 => "23044",
  :代々木公園 => "23045",
  :明治神宮前 => "23016",
  :表参道  => "22588",
  :乃木坂 => "22893",
  :赤坂 => "22485",
  :国会議事堂 => "22668",
  :霞が関 => "22596",
  :日比谷 => "22951",
  :二重橋前 => "22883",
  :大手町 => "22564",
  :新御茶ノ水 => "22732",
  :湯島 => "23038",
  :根津 => "22888",
  :千駄木 => "22782",
  :西日暮里 => "22880",
  :町屋 => "22978",
  :北千住 => "22630",
  :綾瀬 => "22499",
  :北綾瀬 => "22627"
  }

APIを叩くときに、ユーザーから日本語で入力された駅を駅コードに変換しています。
さまざま試した結果、シンボルで渡して文字列の駅コードに変換しています。

送信される駅名を変数として受け取る

@station_name = event.message["text"]

LINEの中で変数に格納する必要がありましたので、この書き方で変数に格納しています。
開発初期はstation_name = gets.chompなどとしていて全然うまく行かなかった。

到着駅を綾瀬に固定してリクエストを投げる

res1 = Net::HTTP.get(URI.parse("http://api.ekispert.jp/v1/json/search/course/light?key=#{ENV['ACCESS_KEY']}&from=#{station_code[@station_name.to_sym]}&to=22499"))

station_nameで受け取った駅名をシンボルに変換
そのシンボルを変数展開することでAPIを叩いています。

叩いて返ってきたJSONをhashに格納

hash = JSON.parse(res1)
url = hash["ResultSet"]["ResourceURI"]

APIを叩くと

"ResultSet":~~
"ResourceURI":~~

という形で返ってくるのでハッシュを使ってurlという変数に格納しています。

スクレイピングして取ってきたテキストを変数に格納

doc = Nokogiri::HTML.parse(html, nil, charset)
doc.xpath('/html/body/div[1]/div[4]/div/div[1]/div[2]/div/table/tr[1]/td[3]/p[1]').each do |node|
$time = node.inner_text
end

chromeのデベロッパーツールで取得したXpathをコピペしても上手く行かず、、。
結局、最初含まれていたtbodyタグを削除すると上手く取れるようになりました。
ブロックの外でtimeを使うのでグローバル変数として格納。(インスタンス変数でも上手くいくかどうは要検証)

苦労した点

構想から開発まで初めて一人でやった点

これまではなにかのチュートリアルに則って開発していたので、ゼロから自分で調べながら開発を進めるのは初めてでした。
分からない単語などは自分で調べたりしながら、実装部分で分からないことは知り合いのエンジニアに教わりながら進めていきました。
テキストコミュニケーションや質問の仕方など、書籍やネットでは拾いづらい基本的な部分が身についたと実感しています。

PullRequestをベースにした開発

チュートリアルをいくらやってもgitの理解はとても難しいなと感じていました。
これまでは、開発済のものをGitHubにpushする経験しかなかったので、ブランチを切りながら開発を進めるという経験ができてとても良かったです。

LINEのメッセージで送られてきたものを変数で受取る処理

コード解説の部分で触れた、ユーザーから送信される駅名を変数に格納する所には苦労しました。
コードを見ればとてもシンプルなのですが、ドキュメントを読んでも見当たらず、この書き方を見つけるのにとても時間がかかりました。
結局、Qiitaで公開されていた記事から似たような実装を見つけてたどり着きました。

sinatraの仕様について

コードの中に

post '/callback' do
end

というブロックがあります。
このブロック中は毎回実行されるという事を知らず、上手く動かなかった時がありました。(未だに出典を見つけられていない)
スクレイピングの部分は毎回実行する必要がありました。
ここの実装についてはRubyコミュニティ(西日暮里.rb、Fukuoka.rb)の皆さんに教えていただき、無事に動くようになりました。

感想

たった100行にも満たないコードですが、自分が「作りたい!」と思ったものが実際に動かせるようになったことにはとても嬉しさを感じています。
まだまだ勉強不足な部分を感じながらも、いい勉強になったと感じています。

実装予定の機能

  • GitHubリポジトリの整理
  • 友達登録時に、「送信できる駅」を一覧を送信する
  • 千代田線ではない駅が送信された時のエラーメッセージの表示

参考

今回の解説記事を書くにあたっては以下の記事を参考にさせていただきました
https://qiita.com/shinbunbun_/items/00c4064c8133a34cf7c3
https://qiita.com/inoue2002/items/7e47283ba9affa0fac82

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

Ruby初心者が2ヶ月で電車の時刻を通知してくれるLINE BOTを作って公開した話

はじめに

「アヤセ教えて君」というLINE Botを開発しました。
まだまだ未完成な部分は多いですが、メインとなる機能の開発を終えたのでGitHub,Qiitaで公開することにしました。
この記事では、機能や技術的な解説をしていきます。
友達追加はこちら
友だち追加

GitHubリポジトリ

https://github.com/Aseiide/ayase_bot

アヤセ教えて君とは

demo.gif

  • 東京メトロ千代田線<下り方面>で活用できます
  • 千代田線において【現在いる駅】を送信すると、綾瀬行電車の時刻が取得できます

開発の経緯

千代田線.png

僕自身、大学・自宅ともに千代田線沿線にあります。
千代田線はJR常磐線に乗入れているため、下りは松戸・柏・取手などかなり遠いところまで繋がっているので夕方ラッシュではかなり混雑しています。
一方、綾瀬行・北綾瀬行の電車は行先が短く混雑している確率はとても低く、僕はこちらに乗るようにしていました。
Yahooの乗り換えアプリで、時刻表のページまで見に行くのが面倒だったので開発の経験を積む意味も含めてLINE Botの開発に取り組むことにしました。

技術解説

技術構成

ayasebot-sequence.png

使用しているAPI

LINE Messaging API

https://github.com/line/line-bot-sdk-ruby
オウム返しのBotを作った事があったのでSDKとドキュメントを参照しながら制作を進めました。

駅すぱあとWeb Service(フリープラン)

個人かつ無料で利用できるこちらのAPIを選定。
フリープランでは、単純に時刻を取得することができないのでAPIを叩いて返ってきたurlに対し、スクレイピングを行って時刻を取得しています。

コード解説

駅名を駅コードに変換

station_code = {
  :代々木上原 => "23044",
  :代々木公園 => "23045",
  :明治神宮前 => "23016",
  :表参道  => "22588",
  :乃木坂 => "22893",
  :赤坂 => "22485",
  :国会議事堂 => "22668",
  :霞が関 => "22596",
  :日比谷 => "22951",
  :二重橋前 => "22883",
  :大手町 => "22564",
  :新御茶ノ水 => "22732",
  :湯島 => "23038",
  :根津 => "22888",
  :千駄木 => "22782",
  :西日暮里 => "22880",
  :町屋 => "22978",
  :北千住 => "22630",
  :綾瀬 => "22499",
  :北綾瀬 => "22627"
  }

APIを叩くときに、ユーザーから日本語で入力された駅を駅コードに変換しています。
さまざま試した結果、シンボルで渡して文字列の駅コードに変換しています。

送信される駅名を変数として受け取る

@station_name = event.message["text"]

LINEの中で変数に格納する必要がありましたので、この書き方で変数に格納しています。
開発初期はstation_name = gets.chompなどとしていて全然うまく行かなかった。

到着駅を綾瀬に固定してリクエストを投げる

res1 = Net::HTTP.get(URI.parse("http://api.ekispert.jp/v1/json/search/course/light?key=#{ENV['ACCESS_KEY']}&from=#{station_code[@station_name.to_sym]}&to=22499"))

station_nameで受け取った駅名をシンボルに変換
そのシンボルを変数展開することでAPIを叩いています。

叩いて返ってきたJSONをhashに格納

hash = JSON.parse(res1)
url = hash["ResultSet"]["ResourceURI"]

APIを叩くと

"ResultSet":~~
"ResourceURI":~~

という形で返ってくるのでハッシュを使ってurlという変数に格納しています。

スクレイピングして取ってきたテキストを変数に格納

doc = Nokogiri::HTML.parse(html, nil, charset)
doc.xpath('/html/body/div[1]/div[4]/div/div[1]/div[2]/div/table/tr[1]/td[3]/p[1]').each do |node|
$time = node.inner_text
end

chromeのデベロッパーツールで取得したXpathをコピペしても上手く行かず、、。
結局、最初含まれていたtbodyタグを削除すると上手く取れるようになりました。
ブロックの外でtimeを使うのでグローバル変数として格納。(インスタンス変数でも上手くいくかどうは要検証)

苦労した点

構想から開発まで初めて一人でやった点

これまではなにかのチュートリアルに則って開発していたので、ゼロから自分で調べながら開発を進めるのは初めてでした。
分からない単語などは自分で調べたりしながら、実装部分で分からないことは知り合いのエンジニアに教わりながら進めていきました。
テキストコミュニケーションや質問の仕方など、書籍やネットでは拾いづらい基本的な部分が身についたと実感しています。

PullRequestをベースにした開発

チュートリアルをいくらやってもgitの理解はとても難しいなと感じていました。
これまでは、開発済のものをGitHubにpushする経験しかなかったので、ブランチを切りながら開発を進めるという経験ができてとても良かったです。

LINEのメッセージで送られてきたものを変数で受取る処理

コード解説の部分で触れた、ユーザーから送信される駅名を変数に格納する所には苦労しました。
コードを見ればとてもシンプルなのですが、ドキュメントを読んでも見当たらず、この書き方を見つけるのにとても時間がかかりました。
結局、Qiitaで公開されていた記事から似たような実装を見つけてたどり着きました。

sinatraの仕様について

コードの中に

post '/callback' do
end

というブロックがあります。
このブロック中は毎回実行されるという事を知らず、上手く動かなかった時がありました。(未だに出典を見つけられていない)
スクレイピングの部分は毎回実行する必要がありました。
ここの実装についてはRubyコミュニティ(西日暮里.rb、Fukuoka.rb)の皆さんに教えていただき、無事に動くようになりました。

感想

たった100行にも満たないコードですが、自分が「作りたい!」と思ったものが実際に動かせるようになったことにはとても嬉しさを感じています。
まだまだ勉強不足な部分を感じながらも、いい勉強になったと感じています。

実装予定の機能

  • GitHubリポジトリの整理
  • 友達登録時に、「送信できる駅」を一覧を送信する
  • 千代田線ではない駅が送信された時のエラーメッセージの表示

参考

今回の解説記事を書くにあたっては以下の記事を参考にさせていただきました
https://qiita.com/shinbunbun_/items/00c4064c8133a34cf7c3
https://qiita.com/inoue2002/items/7e47283ba9affa0fac82

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

CircleCI + Capistrano + AWS(EC2) + Railsで自動デプロイしてみた

はじめに

先日、Capistranoを使って自作のポートフォリオをAWSにデプロイしたので、CircleCIと組み合わせて自動デプロイしてみました。

筆者はプログラミング学習を始めて4ヶ月ぐらいですが、一週間ほどで実装できました。

ポートフォリオにCircleCI/CDを組み込んでみたい!という方の参考になれば嬉しいです。

ご指摘等あれば、コメントいただければ幸いです。

前提

  • Railsアプリ作成済
  • CIrcleCIによる自動テストを導入済
  • Capistranoを使ってAWSにデプロイ済

CircleCIとCapistranoに関しては別の記事でまとめています。

CircleCIでSystemSpec(RSpec)とRubocopを走らせる)
Capistrano + AWS(EC2) + Rails で簡単デプロイ

手順

複数の設定をする必要があるので、順を追ってやっていきましょう。

  1. CircleCIにssh秘密鍵を登録して、AWSのEC2インスタンスにアクセスできるようにする
  2. CircleCIのコンソール上で環境変数を設定する
  3. .circleci/config.ymlを編集して、SSH接続できることを確認する
  4. GithubへpushしたときにCapistranoデプロイを走らせる
  5. Githubのブランチがmasterの時にだけCapistranoデプロイを走らせるように設定する

1.CircleCIにSSH秘密鍵を登録

CIrcleCIのGUI上で使用するプロジェクトを選択した後に"Project settings >> SSH KEYS >> Add SSH key" を選択してHost NamePrivate Keyを記入します。

Host Nameはドメイン、またはIPを記述します。
筆者は独自ドメインを取得していたのでappname.comを記述しました。

Private Keyは秘密鍵の中身を記述します、
ただ、ここで注意点が2点があるので説明します。(筆者はここで2〜3日詰まりました...)

秘密鍵を登録する時の注意点

1. ローカルからEC2にログインする際に使用する秘密鍵の中身を記述する

自作アプリを作成してAWSにデプロイまでやっていると、AWSアクセスキーやGithubとの紐付けのための秘密鍵など、複数の秘密鍵があるはずです。
そのため、どの秘密鍵を使用すればいいのかが迷うかもしれません。(筆者は迷いました...)

必要な鍵はローカルからEC2にログインする際に使用する秘密鍵です。

もし、秘密鍵を~/.sshに格納している場合、秘密鍵は以下のコマンドで一覧が確認します。

(local)
[~]$ cd ~/.ssh
[.ssh]$ ls

筆者の場合はローカルからEC2にログインする際に以下のコマンドを使用します。

(local)
[~]$ ssh appname_rsa

その場合、ターミナル上で以下のコマンドを打ち、秘密鍵の中身をコピーできます。

(local)
[~]$ pbcopy < ~/.ssh/appname_rsa

コピーした中身をPrivate Keyにペーストしましょう。

そして、記述内容の冒頭が-----BEGIN RSA PRIVATE KEY-----であればOK
です。

もし、-----BEGIN OPENSSH PRIVATE KEY-----であれば次の注意点に進みましょう。

2. 秘密鍵のファイル形式はPEM形式でなければならない

SSH Keyのファイル形式はOPENSSHPEMがあり、CircleCIに設定するSSH Keyのファイル形式はPEMに指定されています。

ファイル形式の見分け方は秘密鍵の中身の冒頭部分でわかります。
OPENSSHの場合:-----BEGIN OPENSSH PRIVATE KEY-----
PEMの場合:-----BEGIN RSA PRIVATE KEY-----

もし、ローカルからEC2にログインする際に使用する秘密鍵がOPENSSH形式だった場合は、PEM形式の秘密鍵を作成し、EC2にログインできるように設定する必要があります。

筆者はOPENSSH形式で作成していたので、次の手順で秘密鍵を作成しなおしました。

PEM形式の秘密鍵の作成・ログイン設定方法

まずローカルで鍵の生成を行います。

(local)
[~]$ cd .ssh
[.ssh]$ ssh-keygen -m pem
(#公開鍵を作成)
-----------------------------
Enter file in which to save the key ():appname_rsa 
(#ここでファイルの名前を記述して、エンター)
Enter passphrase (empty for no passphrase): 
(#何もせずそのままエンター)
Enter same passphrase again: 
(#何もせずそのままエンター)
-----------------------------

[.ssh]$ ls
#「appname_rsa」と「appname_rsa.pub」が生成されたことを確認
[.ssh]$ cat appname_rsa.pub
(#鍵の中身をターミナル上に出力→ssh-rsa~~~~localまでをコピーしておく)

次にサーバー側(EC2)で先ほど作成した公開鍵を設定します。

(server)
[yuki|~]$ mkdir .ssh
[yuki|~]$ chmod 700 .ssh
[yuki|~]$ cd .ssh
[yuki|.ssh]$ vim authorized_keys
(#vimが開く)
-----------------------------
ssh-rsa sdfjerijgviodsjcIKJKJSDFJWIRJGIUVSDJFKCNZKXVNJSKDNVMJKNSFUIEJSDFNCJSKDNVJKDSNVJNVJKDSNVJKNXCMXCNMXNVMDSXCKLMKDLSMVKSDLMVKDSLMVKLCA -------@--------no-MacBook-Air.local
(#先ほどコピーした鍵の中身を貼り付け)
-----------------------------
[yuki|.ssh]$ chmod 600 authorized_keys
[yuki|.ssh]$ exit
[ec2-user|~]$ exit

完了したら、ローカルに戻って鍵をどの通信の認証時に使用するか等を設定します。

(local)
[~]$ cd .ssh
[.ssh]$ vim config
(#Vimを起動し、設定ファイルを編集する)

# 以下を追記
Host appname_rsa
  Hostname EC2のElastic IP (#自分の設定に合わせて)
  Port 22
  User yuki (#EC2のユーザー名)
  IdentityFile ~/.ssh/appname_rsa (#秘密鍵の設定)
-----------------------------

これでPEM形式の秘密鍵を使ったSSH通信が可能になります。
ローカルで下記コマンドを入力し、実際にログインできるか試してみましょう。

(local)
ssh appname_rsa

ログインできれば、設定完了です。

ここまでできたら、注意点1を参考にしてCircleCIのPrivate Keyに秘密鍵の中身を記述して登録しましょう。

2. CircleCIのコンソール上で環境変数を設定する

CircleCIはGithubのソースコードをベースにデプロイを行います。
そのため、gitignoreに記述されているようなGithub上にpushされていないファイルは認識できません。

そしてCircleCIではコンソール上で環境変数として設定することで、そういったファイルを管理する機能があるのでそれを利用します。

CircleCIのプロジェクトの設定からEnvironment Variablesのページへ行き、Add Variableを選択します。

そして、二つの環境変数を設定します。

Name:'RAILS_MASTER_KEY' Value: ローカルにある'master.key'の中身を記述。

Name:'PRODUCTION_SSH_KEY' Value:'~/.ssh/appname_rsa_xxxxxxxxxxxxxxx~'

PRODUCTION_SSH_KEYのValueのappname_rsa_の後ろには、先ほど登録したSSH KeyのHost Nameの隣に記述されているFingerprintsの:抜きの文字列を記述してください。

次に本番環境でのCapistranoのSSH接続設定を行います。
ここでは、PRODUCTION_SSH_KEYを使ってconfig/deploy/production.rbを記述しましょう。

config/deploy/production.rb
server 'EC2のElastic IPを記述', user: 'yuki', roles: %w[app db web]

# CircleCIのGUIで設定した環境変数を使ってSSH接続
set :ssh_options, {
  keys: [ENV.fetch('PRODUCTION_SSH_KEY').to_s],
  forward_agent: true,
  auth_methods: %w[publickey]
}

3. SSH通信できることを確認する

.circleci/config.ymlに以下の記述を追加しましょう。
Fingerprintsには先ほど登録したSSH KeyのHost Nameの隣に記述されているのでコピペしてください。

.circleci/config.yml
     - add_ssh_keys:
          fingerprints:
            - "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX"

そしてgithubへpushしてみましょう。

実行できていれば、CircleCI側のコンソールで、Installing additional ssh keysという処理に対して

Installed key XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX

と表示されるはずです。

4. GithubへpushしたときにCapistranoを走らせる

.circleci/config.ymlに以下の記述をします。

.circleci/config.yml
      - add_ssh_keys:
          fingerprints:
            - "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX"

      - deploy:
          name: Capistrano deploy
          command: bundle exec cap production deploy

Githubにpushしてみましょう。
自動デプロイできるはず!...です。

5. Githubのブランチがmasterの時にだけCapistranoデプロイを走らせるように設定する

circleci/config.ymlのworkflowsの最後にfilters追記します。

.circleci/config.yml
# build,test,deployを記述。




workflows:
  version: 2
  build_accept_deploy:
    jobs:
      - build
      - test:
          requires:
            - build
      - deploy:
          requires:
            - test
          filters:
            branches:
              only: master

これでGithubのブランチがmasterの時にだけCapistranoデプロイが走るようになります!

まとめ

筆者はSSH認証の部分でかなりつまずきました笑。

同じようにつまづいている方の手助けになれば嬉しいです☺️

最後にCircleCIとCapistranoのソースコードと参考記事を載せておくのでご参考までに。

ソースコード

.circleci/config.yml
version: 2.1

orbs:
  ruby: circleci/ruby@1.1.0

jobs:
  build:
    docker:
      - image: circleci/ruby:2.5.1-node-browsers
        environment:
          BUNDLER_VERSION: 2.1.4
    steps:
      - checkout
      - ruby/install-deps

  test:
    parallelism: 3
    docker:
      - image: circleci/ruby:2.5.1-node-browsers
        environment:
          DB_HOST: 127.0.0.1
          RAILS_ENV: test
          BUNDLER_VERSION: 2.1.4
      - image: circleci/mysql:8.0
        command: --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          MYSQL_ROOT_HOST: '%'
    steps:
      - checkout
      - ruby/install-deps
      - run: mv config/database.yml.ci config/database.yml 
      - run:
          name: Wait for DB
          command: dockerize -wait tcp://localhost:3306 -timeout 1m
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      # Run rspec in parallel
      - ruby/rspec-test
      - ruby/rubocop-check

  deploy:
    docker:
      - image: circleci/ruby:2.5.1-node-browsers
        environment:
          BUNDLER_VERSION: 2.1.4
    steps:
      - checkout
      - ruby/install-deps
      - add_ssh_keys:
          fingerprints: "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX"
      - deploy:
          name: Capistrano deploy
          command: bundle exec cap production deploy


workflows:
  version: 2
  build_accept_deploy:
    jobs:
      - build
      - test:
          requires:
            - build
      - deploy:
          requires:
            - test
          filters:
            branches:
              only: master
config/deploy.rb
# capistranoのバージョン固定
lock '3.14.1'

# デプロイするアプリケーション名
set :application, 'golfour'

# cloneするgitのレポジトリ
set :repo_url, 'git@github.com:xxxxxx/xxxxxx.git'

# deployするブランチ。デフォルトはmasterなのでなくても可。
set :branch, 'master'

# deploy先のディレクトリ。
set :deploy_to, '/var/www/rails/appname'

# secret_base_keyを読み込ませるため追記
set :linked_files, %w[config/master.key]

# シンボリックリンクをはるファイル。
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/settings.yml', '.env')

# シンボリックリンクをはるフォルダ。
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')

# 保持するバージョンの個数。過去5つまで履歴を保存。
set :keep_releases, 5

# rubyのバージョン
set :rbenv_ruby, '2.5.1'

# 出力するログのレベル。
set :log_level, :debug

namespace :deploy do
  desc 'Restart application'
  task :restart do
    invoke 'unicorn:restart'
  end

  desc 'Create database'
  task :db_create do
    on roles(:db) do |_host|
      with rails_env: fetch(:rails_env) do
        within current_path do
          execute :bundle, :exec, :rake, 'db:create'
        end
      end
    end
  end

  desc 'Run seed'
  task :seed do
    on roles(:app) do
      with rails_env: fetch(:rails_env) do
        within current_path do
          execute :bundle, :exec, :rake, 'db:seed'
        end
      end
    end
  end

  after :publishing, :restart

  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
    end
  end
end
config/deploy/production.rb
server 'EC2のElastic IP', user: 'yuki', roles: %w[app db web]

# CircleCIのGUIで設定した環境変数を使ってSSH接続
set :ssh_options, {
  keys: [ENV.fetch('PRODUCTION_SSH_KEY').to_s],
  forward_agent: true,
  auth_methods: %w[publickey]
}

参考記事

【circleCI】rails5.2/Capistrano/CICD環境によるAWSへの自動デプロイ
CircleCIでデプロイを自動化

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

【Rails】rakeタスクを実装する

rakeタスクとは

ファイルに記述した処理をコマンドラインから実行する機能です。
ユーザーの属性に応じてステータスを変更する、CSVデータをインポートする、任意のタイミングでユーザーにメールを送る、などなど様々な用途で使われます。

基本的な使い方

タスクファイルを生成

$ rails g task qiita_task

実行したい処理を記述

namespace :qiita_task do
  desc 'hello worldします'
  task :hw do
    puts 'Hello World'
  end
end

実行

$ rake qiita_task:hw

その他

タスクの中にDBに接続する処理が含まれる場合

DBに接続する場合、以下のようにenvironmentと記述する

namespace :qiita_task do
  desc '最近登録したユーザーにメールを送信'
  task send_email_to_recent_users: :environment do
    recent_users = User.where('updated_at <= ?', Time.zone.parse('2020/09/08 15:50:00'))
    recent_users.each do |ru|
      ru.send_email
    end
  end   
end

本番環境で実行する

プロジェクトのルートディレクトリ(Gemfileとかがあるとこ)で、RAILS_ENV=productionをつけて実行。

$ rake qiita_task:hw RAILS_ENV=production

タスク一覧を表示

デフォルトで定義されているタスクと、自分が作成したタスクがずらっと表示されます。

$ rake -T

参考

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

commit failed exit code 1のエラー解決方法

【概要】

1.結論

2.なぜ"commit failed exit code 1"になるのか

3.どのように解決するか

4.ここから学んだこと

1.結論

git cloneしたファイルを削除する。


2.なぜ"commit failed exit code 1"になるのか

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

以前に

ターミナル
% git clone https://github.com/"githubのアカウント名"/"githubのリモートレポジトリファイル名”.git

行ったことが原因でした。
上記のようにgitcloneしたものがcommitしたいファイルに残っていたせいでした。


3.どのように解決するか

該当ファイルの中の場所に"clone_site"と作ってありました。そのファイルを削除して、再度新たにgithubに反映させたいファイルを指定してcommitするとエラーは解消しました。

4.ここから学んだこと

当時git cloneした際は一瞬だけ少し試したいことがあったため使用しました。なので、用済みであればそのファイルは消すか、commitしない自信がそのファイルは使用しない方向にするべきでした。そうしないと、今回のようにgit cloneしたことを忘れかけていたので土壺にはまり時間をうばわれかねません。

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

deviseを用いたユーザー認証機能の実装(3)

ユーザー新規登録

ビューから送られてくる情報をUserテーブルに保存するには、
form_withでmodelのインスタンス変数とurlを指定します。

registrations/new.html.erb
<%= form_with model:@user, url:"/users", local: true do |f| %>
 #省略
<% end %>

ここでのmodel:に指定している@userはUserモデルに基づくインスタンス変数で、deviseのコントローラーの中で宣言されています。
deviseのコントローラーは通常見えなくなっていますが、下記コマンドで生成して、カスタムすることができるようです。

$ rails g devise:controllers users

url:に指定しているのは、下記のルーティングで設定されているURI Patternの部分です。

Prefix Verb   URI Pattern             Controller#Action                                                                 
       POST   /users(.:format)        devise/registrations#create

またemailとpassword以外のカラムに情報を保存する場合は下記の記述でカラムへの保存を許可しないといけません。下記の例ではsign_up時にkeys: [:username]でusernameカラムへの保存を許可しています。

application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
  end
end

参考:https://github.com/heartcombo/devise#strong-parameters

ログアウト

ログアウト機能の実装は、ビューファイル上に下記のようにログアウトのリンクを用意します。

<%= link_to 'ログアウト', destroy_user_session_path, method: :delete, class: "logout" %>

ルーティングの設定と同じようにpathとmethod:を設定してあげればOKです。

Prefix               Verb   URI Pattern                Controller#Action
destroy_user_session DELETE /users/sign_out(.:format)  devise/sessions#destroy

ログイン

ログイン機能の実装は、ビューファイル上に下記のようにログインのリンクを用意します。

<%= link_to 'ログイン', new_user_session_path, class: "login" %>

ログイン情報を入力させるビューのフォームでは下記のように記述します。

sessions/new.html.erb
<%= form_with model:@user, url:user_session_path, class: 'registration-main', 
  #省略
<% end %>

リクエストを送るurl:を見て分かる通り、
ログインのリンクがクリックされた時はdevise/sessionsのnewアクションが実行され、
ログイン情報を入力して、submitされた時はdevise/sessionsのcreateアクションが実行されてログインできます。

Prefix           Verb   URI Pattern                Controller#Action
new_user_session GET    /users/sign_in(.:format)   devise/sessions#new
    user_session POST   /users/sign_in(.:format)   devise/sessions#create

簡単な説明ではありますが、以上でdeviseを用いたユーザー認証機能の最低限の機能は実装できました。
ご覧いただき、有難うございました。

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

【Rails】アソシエーションの種類(1対多 / 多対多)

脳内整理のために

プログラミング初学者がRailsを学び始めて最初に、ん?となるポイント(のはず)であるアソシエーション。
アソシエーションについて個々の記事はいくらでもあるので、全体像としてその種類を整理してみました。

アソシエーションの種類

アソシエーション表.png
表の通りに分類できます。(今回1対1のアソシエーションは省略しました。)

まとめ

  • 多対多は中間テーブルが必要
  • 自己結合は同じモデルを参照する外部キーが必要
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】関数における例外処理

はじめに

この記事は数多ある例外処理についての補足的なものです。具体的には、記事で紹介されてないパターンについて検証した結果を残します。

環境

  • Mac
  • Ruby 2.6.3

関数の中での使用

関数の中では以下のように書けます。

def method
  puts "hoge"
  a = 1 / 0
rescue ZeroDivisionError
  puts $!
end

begin, endを省いた形ですね。このような形の場合、

a = 1 / 0

がエラーを発生させるであろうことは一目瞭然です。しかし、以下の場合であればどうでしょうか。

def method
  # 膨大な処理1
  # エラーが発生しそうな処理
  # 膨大な処理2
rescue StandardError
  puts $!
end

さて、このコードを見た人がすぐにエラーの発生しそうな処理を見抜けるかというと怪しいところです。その場合はあえてbegin, endを書いたらいいと思います。

def method
  # 膨大な処理1
  begin
    # エラーが発生しそうな処理
  rescue StandardError
    puts $!
  end
  # 膨大な処理2
end

こうすればどこがエラー発生しそうか分かりますし、沢山スクロールしてrescueを見に行く必要もありません。
ちなみにrescueでreturnすると膨大な処理2は処理されないので、間違いなく処理してほしいならensureに入れましょう。

def method
  # 膨大な処理1
  begin
    # エラーが発生しそうな処理
  rescue StandardError
    puts $!
    return
  ensure
    # 膨大な処理2(rescueにreturnがあっても処理される)
  end
end

もしエラーの発生しそうな処理が複数ある場合は、関数の最後にまとめて書くのがシンプルでいいと思います。毎回begin, rescue, end書くのは流石に可読性下がりますので。何事も適切に。

def method
  # いろいろエラーが発生しそうな処理

  # その他処理

rescue ZeroDivisionError, ArgumentError
  puts $!
rescue StandardError
  puts $!
  puts $@
end

ちなみにStandardErrorを先に書くと大体のエラー処理はこいつがやってしまいます。なので子クラスから書いていきましょう。

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

[Rails] ActiveRecord::HasManyThrough Order Error in Users#show

環境

Rails 6.0.3.2
ruby 2.6.5p114 (2019-10-01 revision 67812)
vscode

本稿の趣旨

https://qiita.com/tenitiumai/items/3d9466d7a24197f690bb 
を参考にしてDM機能を作成することを目標とする。

 しかし、本稿は、エラーの解決方法を紹介するため、
具体的なDM機能の作成手順については割愛する(上記の参考記事を参照していただきたい)。

該当のエラー

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

エラーの原因

参考記事 https://qiita.com/krppppp/items/0db4184e9df553f05048
これは、 Cannot have a has_many:through association 'User#followers' which goes through 'User#follower_relationships' before the through associations is defined. に注目する。
どうやら、モデルの配置順序が間違っていることが原因である。
具体的には、follower_relationshipsが読み込まれる前にfollowersが読み込まれているため、エラーが生じていると推測できる。

解決方法

コードを見ると、

app/models/user.rb
  has_many :followers, through: :follower_relationships

  has_many :follower_relationships, foreign_key: "following_id", class_name: 
  "Relationship", dependent: :destroy

  has_many :followings, through: :following_relationships

となっており、followersが、follower_relationshipsより上にある。これを

app/models/user.rb
  has_many :followings, through: :following_relationships

  has_many :follower_relationships, foreign_key: "following_id", class_name: 
  "Relationship", dependent: :destroy

  has_many :followers, through: :follower_relationships

とすれば、解決するのではないか。
DMボタンを配置したユーザー詳細ページにアクセスすると、
スクリーンショット 2020-09-09 13.15.47.png

無事解決しました。

おわりに

モデルの順序でエラーが生じることになるとは思わなかった。
これからはその点も意識していこうと思いました。

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

ネストした配列の同じIndex同士の足し算のやり方

やりたいこと

numbers = [
 [1,2,3],
 [4,5,6],
 [7,8,9]
]

こんな配列から同じIndex同士で足し算して以下のような配列を取得する方法を考えていました。

sums = [12, 15, 18]

結論

numbers = [[1,2,3], [4,5,6], [7,8,9]]
sums = numbers.transpose.map{ |num| num.sum }

です。

はじめはネストした「配列の0番目同士をどうやって取得するか」と考えていて、効率の良い取得方法に悩んでいました。が、行列と考えて転置させれば一発だったのですね。

行列の転置はRubyの場合 transpose で可能です。参考:instance method Array#transpose

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

脱初心者のためのもっと使えるEnumerable

はじめに

この記事は、筆者が実際に業務でプルリクレビューをすると指摘することが多いEnumerableに関連するあれこれについて書こうと思います。
すべてのメソッドを網羅するわけではないので悪しからず。

英語版はこちら

Enumerable とは 

繰り返しを行なうクラスのための Mix-in。このモジュールのメソッドは全てeachを用いて定義されているので、インクルードするクラスには each が定義されていなければなりません。

https://docs.ruby-lang.org/ja/latest/class/Enumerable.html
とのことです。つまり、普段eachを使うことのあるクラス(例: Array, Hash, String)は、このEnumerableモジュールをインクルードしています。

eachを見つけたら使い所

each処理を見つけたら、eachを使わずに同じことをかける可能性があります。
一旦立ち止まってeachを使わずにかけないか探してみましょう。
また、やりたいことによっては、別のEnumerableメソッドを使うとよりスッキリする可能性があります。

実践Enumerable

難しいことは置いておいて実際に僕が指摘することが多いものをまとめたので見ていきましょう。
業務よりのものはちょっと変数に意味を持たせるなどしてみます。

Case 1: select

Before

arr = [1, 2, 3, 4, 5]
new_arr = []
arr.each do |v|
  new_arr << v if v.odd?
end

p new_arr # => [1, 3, 5]

After

new_arr = arr.select(&:odd?)

p new_arr # => [1, 3, 5]

Case 2: map

Before

arr = [1, 2, 3, 4, 5]
new_arr = []
arr.each do |v|
  new_arr << v * 2
end

p new_arr # => [2, 4, 6, 8, 10]

After

new_arr = arr.map { |v| v * 2 }

p new_arr # => [2, 4, 6, 8, 10]

Case 3: inject

Before

arr = [1, 2, 3, 4, 5]
sum = 0
arr.each do |v|
  sum += v
end

p sum # => 15

After

arr = [1, 2, 3, 4, 5]
sum = arr.inject(:+)

p sum # => 15

Case 4: any?

前提

予約ステータスが下記のように定義されている。
ステータス変更が正しいかバリデーションしたい

booking_statuses = {
  pending: 0,
  payment_requested: 1,
  paid: 2,
  cancelled: 3
}

Before

def validate_booking_transition(passed_status)
  if passed_status == booking_statuses[:cancelled]
    allowed = [
      booking_statuses[:pending],
      booking_statuses[:payment_requested],
      booking_statuses[:paid]
    ].include?(passed_status)
  elsif ...
  .
  .
  .
end

After

def validate_booking_transition(passed_status)
  if passed_status == booking_statuses[:cancelled]
    allowed = %i(pending payment_requested paid).any? do |v|
      passed_status == booking_statuses[v]
    end
  elsif ...
  .
  .
  .
end

Case 5: group_by

Before

arr = [{code: 'a', val: 1}, {code: 'a', val: 2}, {code: 'b', val: 3}, {code: 'b', val: 4}]
new_hash = {}
arr.each do |hash|
  k = hash[:code]
  new_hash[k] = [] if new_hash[k].nil?

  new_hash[k] << hash[:val]
end

p new_hash #=> {"a"=>[1, 2], "b"=>[3, 4]}

After

new_hash = arr.group_by { |h| h[:code] }.transform_values { |grouped_arr| grouped_arr.map { |h| h[:val] } }

p new_hash #=> {"a"=>[1, 2], "b"=>[3, 4]}

考察

それぞれについて少し考察してみましょう。
Case 1 ~ 3に関しては、よくあるeachでやってるその処理、便利メソッドですっきりかけるよというパターンです。ほぼEnumerableの紹介ですね。
Case 4は、Enumerableの使い所の話で、やりたいことによってEnumerableメソッドを使い分けることで、冗長になっているものがより簡潔にかけるようになるパターンがあります。(イメージをしやすくするためにちょっとビジネスロジック的な要素を入れています)
Case 5に関してはどうでしょうか。これに関しては、単純に一行でかけてスッキリ!な気もしますが、分かりにくくなっている気がしますね。
もっとよく見てみると, Case 1 ~ 4では、繰り返し処理に置いて、

  • Case 1: 配列から条件にあった要素の配列を取り出す
  • Case 2: 配列の各要素に2をかける
  • Case 3: 配列の要素を足し合わせる
  • Case 4: 配列の要素の中で、条件に当てはまるものがあるか判定する

というように1つのことを実行しているのに対し、Case 5では

  • codeをkeyとした新しいハッシュを生成し、
  • 各codeごとにvalの配列を作る という複数の処理を行っていることが分かります(さらに言えば、配列から、ハッシュという別のオブジェクトを生成しています)

さらに詳しく見ていくと、transform_valuesのブロックの中でさらにmapを実行しています。一見シンプルに書けているようで、2重ループを作り出してしまっていますね。
このように複数のことを一回の繰り返し処理の中で行う必要がある場合、eachを使ったほうがわかりやすさ、さらに処理速度の面でも優位性がある可能性があります。

Enumerableメソッドを正しく使うメリットはなんなのか

多くの開発現場に置いて、コードを書く上で最も大切な指標は、可読性・そしてメンテナンスのしやすさではないかと思います。
eachメソッドは便利な半面、それ自体が意味を持たないため(基本的にただループを回すだけ)、時にコードの書き手が何を意図しているのかが読み手に伝わらない可能性があります。Enumerableメソッドを正しく使えば、書き手の意図が明確になり、将来のコード変更も容易になるでしょう。
可読性以外の視点でいうと、作用・副作用というものをより明確に意識できるようになると思います。が、それは長くなりそうなのでここでは割愛します。
ただし、Case 5で示したように、eachを使ったほうが結果的によいというパターンも有り得ますので(特にEnumerableメソッドの中でさらにEnumerableメソッドを使っている場合は要注意)気をつけてみましょう。

終わりに

いかがでしょうか。今回はEnumerableメソッドについて例とともに簡単に説明してみました。
使いこなせるようになってもっと気持ちよくコードを書いていきましょう;)

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

deviseを用いたユーザー認証機能(2)

それでは次にブラウザで

http://localhost:3000/users/sign_up

にアクセスしてみましょう。

deviseにてデフォルトで用意されている味気ないビューファイルがレンダリングされているはずです。
スクリーンショット 2020-09-09 11.18.20.png

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

deviseを用いたユーザー認証機能の実装(2)

それでは次にブラウザで

http://localhost:3000/users/sign_up

にアクセスしてみましょう。

deviseにてデフォルトで用意されているビューファイルがレンダリングされているはずです。


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


このままだと味気ないのでビューファイルをカスタマイズしたいですよね。
なので、ターミナルで

% rails g devise:views

を実行します。そうするとdevise関連のviewファイルが生成されます。
そちらを編集して、自分好みの見た目に仕上げましょう。

(3)へ続く

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

Rails 6で認証認可入り掲示板APIを構築する #4 postのバリデーション、テスト実装

Rails 6で認証認可入り掲示板APIを構築する #3 RSpec, FactoryBot導入しpostモデルを作る

RSpecでmodelのテストを書く

とりあえずcreate, readが動くのは前回確認できました。
ここからはこの手順でいきます

  1. postのmodelテストを書く
  2. validationを実装する
  3. postのcontrollerテストを書く
  4. controller, routesを書く
  5. seedを書く

この記事ではとりあえず1.と2.の実装をして、3.以降はまた次回以降の記事で進めていきます。

rubocopをあらかじめつぶしておく

ドキュメンテーション書きなさいエラーがmigrationファイルにも出てくるので除外します。

.rubocop.ymlはこのように除外設定もできますが、本来必要なものも面倒がって除外していくとそもそもコーディング規約を守る意味が崩れるので、チーム開発で追加する際はしっかり議論しましょう。

.rubocop.yml
+ # ドキュメンテーション
+ Style/Documentation:
+  Exclude:
+    - "db/migrate/**/*"
...

まずmodelテストを書く

テスト駆動開発(TDD)っぽく、まずはRedのテストです。
バリデーション未実装なのでテストを書いて動かしてもRedとなるものを作ります。

一旦factory_bot使わずに普通のRailsチックに書いてみます。

spec/models/post_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Post, type: :model do
  describe "subject" do
    context "blankの時に" do
      it "invalidになる" do
        post = Post.new(subject: "", body: "fuga")
        expect(post).not_to be_valid
      end
    end
  end
end
  • describeはテスト対象を示します
  • contextは条件を示します
  • it(もしくはexample)はテスト対象を示します
  • expect(post).not_to be_validは、postがbe_validと等しくないことをテストしています

なので上記コードは『subjectがblankの時にinvalidになる』ことをテストしています。
ですが今のところpostモデルのsubjectにバリデーションを入れていないのでpostは有効であり、「invalidになる」テストは失敗することになります。

$ rspec spec/models/post_spec.rb
...
Finished in 0.07805 seconds (files took 3.53 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/models/post_spec.rb:8 # Post subject blankの時に invalidになる

ec2-user:~/environment/bbs (master) $ rspec

本当にsubjectが空でも登録できるか試してみましょう。

$ rails c
[1] pry(main)> Post.create!(subject: "", body: "hoge")
   (0.1ms)  BEGIN
  Post Create (2.5ms)  INSERT INTO "posts" ("subject", "body", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["subject", ""], ["body", "hoge"], ["created_at", "2020-09-06 01:07:52.628768"], ["updated_at", "2020-09-06 01:07:52.628768"]]
   (0.9ms)  COMMIT
=> #<Post:0x0000000005760700
 id: 2,
 subject: "",
 body: "hoge",
 created_at: Sun, 06 Sep 2020 01:07:52 UTC +00:00,
 updated_at: Sun, 06 Sep 2020 01:07:52 UTC +00:00>

保存できてしまいましたね。

ちなみにdescribeやcontextを使わずに

spec/models/post_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Post, type: :model do
  it "subjectがblankの時にinvalidになる" do
    post = Post.new(subject: "", body: "fuga")
    expect(post).not_to be_valid
  end
end

というコードでもほぼ同じテストコードの挙動になります。なぜなら、itブロックでexpectを書けばテストができるためです。
しかし同カラムや同バリデーション条件等をグルーピングして記述する際に分かりづらくなるので、基本的にdescribeやcontextを入れ子にして記述するのがオススメです。

modelにバリデーションを追加する

blankをエラーとするバリデーション

app/models/post.rb
 class Post < ApplicationRecord
+  validates :subject, presence: true
 end

これで、subjectカラムに対してpresence(存在)に対してtrue, つまりblankでの登録ができなくなります。

試してみましょう。

$ rails c
[1] pry(main)> Post.create!(subject: "", body: "hoge")
ActiveRecord::RecordInvalid: Validation failed: Subject can't be blank
from /home/ec2-user/.rvm/gems/ruby-2.7.1/gems/activerecord-6.0.3.2/lib/active_record/validations.rb:80:in `raise_validation_error'

登録できないですね。

$ rspec ./spec/models/post_spec.rb 
...
Finished in 0.05053 seconds (files took 1.63 seconds to load)
1 example, 0 failures

テストも通過しました。

最大文字数のバリデーション

文字数が無限に登録できると困るので制限を加えます。
こちらも先にテストから。
30文字以内ならOK、31文字以上はNGというバリデーションを追加する予定でテストを書いてみます。

spec/models/post_spec.rb
         expect(post).not_to be_valid
       end
     end
+    context "maxlengthにより" do
+      context "30文字の場合に" do
+        it "validになる" do
+          post = Post.new(subject: "あ" * 30, body: "fuga")
+          expect(post).to be_valid
+        end
+      end
+      context "31文字の場合に" do
+        it "invalidになる" do
+          post = Post.new(subject: "あ" * 31, body: "fuga")
+          expect(post).not_to be_valid
+        end
+      end
+    end
   end
 end

テスト実行してみましょう。

$ rspec ./spec/models/post_spec.rb 
...
Finished in 0.03204 seconds (files took 1.42 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./spec/models/post_spec.rb:21 # Post subject maxlengthにより 31文字の場合に invalidになる

まだvalidationを追加していないので、30文字はパスしますが31文字はコケますね。
modelにvalidationを追加します。

app/models/post.rb
 class Post < ApplicationRecord
-  validates :subject, presence: true
+  validates :subject, presence: true, length: { maximum: 30 }
 end
$ rspec ./spec/models/post_spec.rb 
...
Finished in 0.02201 seconds (files took 1.4 seconds to load)
3 examples, 0 failures

テスト通りましたね。
これで31文字の際にエラーになります。rails cで試してみると良いでしょう。

FactoryBotに置き換える

例えば先程のテストコードにある

  post = Post.new(subject: "あ" * 30, body: "fuga")

ですが、毎回body指定するの面倒ですよね。
2カラムであればまだ大丈夫ですが、これが10カラムとか超えてくると無駄にコードが長くなります。
その際にfactoryBotを使います。

factoryBotはspec/factories/下を参照します。
今回はmodelを作った時の初期値から特に変える必要はありませんが、一応中身を見ておきます。

spec/factories/posts.rb
# frozen_string_literal: true

FactoryBot.define do
  factory :post do
    subject { "MyString" }
    body { "MyText" }
  end
end

post_spec.rbファイルを編集します。

spec/models/post_spec.rb
   describe "subject" do
     context "blankの時に" do
       it "invalidになる" do
-        post = Post.new(subject: "", body: "fuga")
+        post = build(:post, subject: "")
         expect(post).not_to be_valid
       end
     end
     context "maxlengthにより" do
       context "30文字の場合に" do
         it "validになる" do
-          post = Post.new(subject: "あ" * 30, body: "fuga")
+          post = build(:post, subject: "あ" * 30)
           expect(post).to be_valid
         end
       end
       context "31文字の場合に" do
         it "invalidになる" do
-          post = Post.new(subject: "あ" * 31, body: "fuga")
+          post = build(:post, subject: "あ" * 31)
           expect(post).not_to be_valid
         end
       end

buildはfactoryBotを使った.newに相当するものです。データベースへの保存は行われません。
今回の場合subjectを指定していますがbodyは未指定なので、factoryBotのbodyは"MyText"が入ります。

また、変更のたびにテスト実行してOKになることを確認してください。

変数をletに置き換える

とりあえず以下のように変更してみてください。

spec/models/post_spec.rb
 RSpec.describe Post, type: :model do
   describe "subject" do
     context "blankの時に" do
+      let(:post) do
+        build(:post, subject: "")
+      end
       it "invalidになる" do
-        post = build(:post, subject: "")
         expect(post).not_to be_valid
       end
     end
     context "maxlengthにより" do
       context "30文字の場合に" do
+        let(:post) do
+          build(:post, subject: "あ" * 30)
+        end
         it "validになる" do
-          post = build(:post, subject: "あ" * 30)
           expect(post).to be_valid
         end
       end
       context "31文字の場合に" do
+        let(:post) do
+          build(:post, subject: "あ" * 31)
+        end
         it "invalidになる" do
-          post = build(:post, subject: "あ" * 31)
           expect(post).not_to be_valid
         end
       end

letは同一describeやcontextのブロック内のスコープに限定される変数です。
Rubyは最後に評価された式が返り値となるので、

  let(:post) do
    build(:post, subject: "あ" * 31)
  end

の場合は、build実行結果のpostが、let(:post)によってpostという変数になります。

演習

bodyにも必須制限・100文字以内制限のテストとバリデーションを実装してみましょう。

body実装回答例
spec/models/post_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Post, type: :model do
  describe "subject" do
    context "blankの時に" do
      let(:post) do
        build(:post, subject: "")
      end
      it "invalidになる" do
        expect(post).not_to be_valid
      end
    end
    context "maxlengthにより" do
      context "30文字の場合に" do
        let(:post) do
          build(:post, subject: "あ" * 30)
        end
        it "validになる" do
          expect(post).to be_valid
        end
      end
      context "31文字の場合に" do
        let(:post) do
          build(:post, subject: "あ" * 31)
        end
        it "invalidになる" do
          expect(post).not_to be_valid
        end
      end
    end
  end

  describe "body" do
    context "blankの時に" do
      let(:post) do
        build(:post, body: "")
      end
      it "invalidになる" do
        expect(post).not_to be_valid
      end
    end
    context "maxlengthにより" do
      context "100文字の場合に" do
        let(:post) do
          build(:post, body: "あ" * 100)
        end
        it "validになる" do
          expect(post).to be_valid
        end
      end
      context "101文字の場合に" do
        let(:post) do
          build(:post, body: "あ" * 101)
        end
        it "invalidになる" do
          expect(post).not_to be_valid
        end
      end
    end
  end
end

この時点でrspec実行するとコケる

app/models/post.rb
# frozen_string_literal: true

#
# 投稿クラス
#
class Post < ApplicationRecord
  validates :subject, presence: true, length: { maximum: 30 }
  validates :body, presence: true, length: { maximum: 100 }
end

rubocopがコケるので除外設定。testはDRYやらコーディング規約やら遵守すると逆効果のこともあるので、あまり厳しくしないほうがいいです。

.rubocop.yml
+ # ブロック長さ
+ Metrics/BlockLength:
+   Exclude:
+     - "spec/**/*"

この時点でrspec, rubocop実行すると通る

続き

→Rails 6で認証認可入り掲示板APIを構築する #5 controller, routes実装

連載目次へ

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

rails tutorial 第7章

はじめに

独学でrails tutorialを進めていく過程を投稿していきます。

進めていく上でわからなかった単語、詰まったエラーなどに触れています。

個人の学習のアウトプットなので間違いなどあればご指摘ください。

初めての投稿なので読みにくいところも多々あるかと思いますがご容赦ください。

第7章 ユーザー登録

7.1.2 Usersリソース

RESTアーキテクチャの習慣に従ってユーザー情報をWebアプリケーション上に表示する。

RESTってなんだっけ、、、


RESTとは、アプリケーションを構成するコンポーネント(ユーザーやマイクロポストなど)を「リソース」としてモデル化することを指します。これらのリソースは、リレーショナルデータベースの作成/取得/更新/削除(Create/Read/Update/Delete: CRUD)操作と、4つの基本的なHTTP requestメソッド(POST/GET/PATCH/DELETE)の両方に対応しています。
コラム2.2(rails tutorial 2章より引用)


つまりはusersモデルをリソースとし、データ(作成、表示、更新、削除)操作とrequestメソッド(POST/GET/PATCH/DELETE)の両方に対応させるとういうイメージでしょうか。

そうすることで
・コントーラーやアクションの決定を簡略化
(リソースとすれば自動で設定してくれる)
・動的なWebページの作成
(ユーザー情報をWebアプリケーション上に表示するなど)
を実現できるということですかね。

usersモデルをリソースとする際はルーティングで

resources :users

と記入するようです。

7.2.2 フォームHTML

新規ユーザーの登録フォームを作りました。分かりにくかった所だけ補足します。

<%= form_with(model: @user, local: true) do |f| %>
    <%= f.label :name %>
    <%= f.text_field :name %>

    <%= f.label :email %>
    <%= f.email_field :email %>

    <%= f.label :password %>
    <%= f.password_field :password %>

    <%= f.label :password_confirmation, "Confirmation" %>
    <%= f.password_field :password_confirmation %>

    <%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>

form_withはデフォルトで“remote” XHR requestを送信しますが、ここではエラーメッセージをほぼ確実に表示するために通常の“local”フォームリクエストを送信したいのです( 7.3.3)。(rails tutorial 7章より引用)


どういうことだろう、、、
“remote” XHR requestって??

調べました。
XHRはAjaxと呼ばれるようなものらしい。
そしてajaxとは非同期通信のことで、ざっくり言うとjs形式のリクエストによってページ遷移を行わずページの一部の表示を変更したりできるものらしい。

参考
https://qiita.com/__tambo__/items/45211df065e0c037d032

rails tutorialではエラーメッセージをほぼ確実に表示するためhtml形式のリクエスト、つまり通常のlocalフォームリクエストを送信したいようです。
なぜremoteリクエストだとエラーメッセージの表示が確実ではないのだろう、、、
まぁ今作成しているsample_appではlocalリクエストで十分というのはわかりますけれど。

余談ですがrails 5以前のform_tagやform_forではデフォルトがリモートフォームではなくローカルフォームだったようです。


次にHTMLのフォームタグについても無知でしたので調べました。

参考記事①
http://www.htmq.com/html5/form.shtml
フォームタグについてのざっくりとした説明。

参考記事②
https://developer.mozilla.org/ja/docs/Learn/Forms/How_to_structure_an_HTML_form
labelついてなども書かれています。


ブロック変数(f)はform_withの第一引数に渡したオブジェクト(今回で言えば@user)の属性に対するinputタグを生成するメソッドを呼び出すもの。

参考
https://rakuda3desu.net/rakudas-rails-tutorial7-2/


Railsは@userのクラスがUserであることを認識します。また、@userは新しいユーザーなので、 Railsはpostメソッドを使ってフォームを構築すべきだと判断します。(rails tutorial 7章より引用)]

この部分についてどういうことか調べました。

参考記事①
https://qiita.com/hmmrjn/items/24f3b8eade206ace17e2
関連するモデルがあり@userがDBに存在するときはupdateアクションに、ないときはcreateアクションに飛ぶということのようです。

参考記事②
https://pikawaka.com/rails/form_with
こちらの記事もわかりやすかったです。

コントローラーで作成したインスタンスがnewメソッドで新たに作成されて何も情報を持っていなければ自動的にcreateアクションへ、findメソッドなどで作成され、すでに情報を持っている場合はupdateアクションへ自動的に振り分けてくれます。(上の記事より引用)

大分よく理解出来た気がします、、、

7.3.1 正しいフォーム

paramsには複数のハッシュに対するハッシュ(hash-of-hashes: 入れ子になったハッシュ)が含まれます。デバッグ情報では、フォーム送信の結果が、送信された値に対応する属性とともにuserハッシュに保存されています。このハッシュのキーが、inputタグにあったname属性の値になります。例えば次のように

<input id="user_email" name="user[email]" type="email" />

"user[email]"という値は、userハッシュの:emailキーの値と一致します。(rails tutorial 7章より引用)]

ここ大事なポイント!!

7.3.2 Strong Parameters

params.require(:user).permit(:name, :email, :password, :password_confirmation)

requireメソッドって?
受け取るパラメータ群を指定

permitメソッドって?
利用可能なパラメータ名を指定

参考
https://techacademy.jp/magazine/22078

7.3.4 失敗時のテスト

演習1
エラー発生!!

test/integration/users_signup_test.rb
#リスト 7.25: エラーメッセージをテストするためのテンプレート
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest

  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_template 'users/new'
    assert_select 'div#<CSS id for error explanation>'
    assert_select 'div.<CSS class for field with error>'
  end
  .
  .
  .
end

リスト7.25を記述してtestを行ったら

ERROR["test_invalid_signup_information", #<Minitest::Reporters::Suite:0x000000000adddc40 @name="UsersSignupTest">, 0.3698949000099674]
 test_invalid_signup_information#UsersSignupTest (0.37s)
Nokogiri::CSS::SyntaxError:         Nokogiri::CSS::SyntaxError: unexpected '#' after '[#<Nokogiri::CSS::Node:0x000000000addfc70 @type=:ELEMENT_NAME, @value=["div"]>]'
            test/integration/users_signup_test.rb:13:in `block in <class:UsersSignupTest>'

とエラーが、、

リスト7.25ので追記されているは

assert_select 'div#<CSS id for error explanation>'
assert_select 'div.<CSS class for field with error>'

以上の2文です。

<CSS id for error explanation>

あ。。。
ここはヒントが書かれているだけであって自分でそのidを書かないといけないじゃん、、、
ということで

assert_select 'div#error_explanation'
assert_select 'div.field_with_errors'

と修正しました。

7.4.1 登録フォームの完成

app/controllers/users_controller.rb
#リスト 7.26: 保存とリダイレクトを行う、userのcreateアクション
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user
    else
      render 'new'
    end
  end

  private

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

redirect_to @user

上記のコードについて何となく意味はわかるが詳しく書かれている記事があるため参考にしました。

参考
https://qiita.com/Kawanji01/items/96fff507ed2f75403ecb

しかし、 チュートリアル5.3.2 によると、Railsの規約では、基本的にはリンクには相対パスを使うべきですが、リダイレクトのリンクでは絶対パスを利用すべきとのことです。(上の記事より引用)

あれそうだったっけ?と調べると

Railsチュートリアルでは一般的な規約に従い、基本的には_path書式を使い、リダイレクトの場合のみ_url書式を使うようにします。これはHTTPの標準としては、リダイレクトのときに完全なURLが要求されるためです。(rails tutorial 5章より引用)]

本当でした、、、

7.4.3 実際のユーザー登録

問題発生!!
データベースをリセットするため、

rails db:migrate:reset

を実行。
しかしリセットできない。

ermission denied @ apply2files - C:/environment/sample_app/db/development.sqlite3
Couldn't drop database 'db/development.sqlite3'
rails aborted!
Errno::EACCES: Permission denied @ apply2files - C:/Users/81801/environment/sample_app/db/development.sqlite3
bin/rails:4:in `require'
bin/rails:4:in `<main>'
Tasks: TOP => db:drop:_unsafe
(See full trace by running task with --trace)

解決
以下の記事を参考に解決しました。
https://teratail.com/questions/67393
https://qiita.com/Toshiki23/items/f366504844fd22ad87d9

どうやらwindowsでデータベースにsqlite3を利用していると
rails dropコマンド
rails db:resetコマンド
rails db:migrate:resetコマンド
において問題が発生してしまうようです。
悲しいかなwindows...

記事にあるようdb/development.sqlite3ファイルをエディタのエクスプローラーより手動で消し(コマンドプロンプトではrmコマンドがフォローされていなかったため)、

rails db:create db:migrate

を改めて実行したところ

Database 'db/development.sqlite3' already exists
Database 'db/test.sqlite3' already exists

あれ?消したはずなのに残っている、、、
使用しているエディタからは確かにdevelopment.sqlite3ファイルは消えている、、、

、、、、どうしよう!!!!!
とりあえず削除したdevelopment.sqlite3ファイルの行方を捜してみました。

参考
https://maitakeramen.hatenablog.com/entry/2018/02/08/131755

ゴミ箱を探してもなぜか見当たらない、、、
(やっぱり削除されていないからか??)

探しに探し、windowsのエクスプローラーからsample_app/dbディレクトリを見てみるとdevelopment.sqlite3ファイルがありました!!

とりあえず開いているエディタのdbディレクトリにコピーしました。

試しにDB Browser for SQliteでデータを覗いて見たらusersテーブルのデータは空になっていました。

原因を調べてみましたがわかりませんでした。
とりあえずデータベースのリセットは達成したこととし、次に進むことにします、、、

余談
この後に、Git Bashを使えばLinuxコマンドも使えるということを教えてもらい、以降はそちらも使って学習を進めることとしました。

7.4.4 成功時のテスト

演習2
演習は問題なくクリア出来ましたが、content_tagヘルパーについて調べました。
content_tagヘルパー
content_tag(:要素名, 表示させる内容, オプション)

参考
https://techacademy.jp/magazine/42167

終わりに

内容が急に難しくなってきました。
tutorialでは当たり前に使われていたUserリソースのような概念を理解するのも大変でした。
form_withヘルパーを使ったフォームの生成も一度ではなかなか理解できず、何度か読み返しました。
度々エラーにも遭遇し、時間がかかりました。

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

【iOS】FastlaneとGoogleスプレッドシートでApp Store説明文の更新を自動化してラクする手順を書いておく。

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、FastlaneとGoogleスプレッドシートを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動マッスィーン 魁、全自動  ぜひ使ってください。  これはとても便利なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto machine Let's Full Auto Please use it. It's a very useful app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。

ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

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

【iOS】GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化してラクする手順を書いておく。

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動マッスィーン 魁、全自動  ぜひ使ってください。  これはとても便利なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto machine Let's Full Auto Please use it. It's a very useful app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。

ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

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

【iOS】GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化してラクする手順を書いておく

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動マッスィーン 魁、全自動  ぜひ使ってください。  これはとても便利なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto machine Let's Full Auto Please use it. It's a very useful app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。

ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

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

GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化してラクする手順を書いておく

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動マッスィーン 魁、全自動  ぜひ使ってください。  これはとても便利なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto machine Let's Full Auto Please use it. It's a very useful app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。

ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

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

GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化する手順を書いておく

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動マッスィーン 魁、全自動  ぜひ使ってください。  これはとても便利なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto machine Let's Full Auto Please use it. It's a very useful app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。

ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

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