20200910のRailsに関する記事は21件です。

Webpackerとは (Rails6で rails s してエラーが出た時の話)

Rails6で新規アプリを作成しようとし、データベース作成後に
rails sでサーバを起動しようとしたところ、以下のようなエラーが出ました。

$ rails s
=> Booting Puma
=> Rails 6.0.3.3 application starting in development 
=> Run `rails server --help` for more startup options
Exiting
Traceback (most recent call last):
    77: from bin/rails:3:in `<main>'
    76: from bin/rails:3:in `load'
          ・
          ・
          ・
Webpacker configuration file not found /Users/<ユーザー名>/<アプリ名>/config/webpacker.yml.
Please run rails webpacker:install
Error: No such file or directory @ rb_sysopen - 
/Users/<ユーザー名>/<アプリ名>/config/webpacker.yml (RuntimeError)

「Webpackerの設定ファイルが見つかりません」とのこと。
/config/webpacker.ymlがあるべきだそう(なかった)。

Webpackerとは

JavaScriptのビルドツール「Webpack」のラッパーで、RailsアプリケーションでWebpackを使ってJavaScriptを管理することを簡単にしてくれるGem
現場で使えるRuby on Rails5 速習実践ガイド

私は「RailsでJavaScriptを使えるようにする為に色々よしなにしてくれるGem」と理解しました・・・。
(この辺りの理解は今後深めていけたら。)

またRailsガイドv6.0によると、「WebpackerがRails 6のデフォルトJavaScriptコンパイラになる」とあり、
rails newした時点で「gem 'webpacker'」がGemfileに書かれている理由がわかります。
以前はSprockets (スプロケッツ)というものがJavaScriptの標準ビルドツールとなっていたようです。

rails webpacker:installしてみる

どうしてGemfileに書いてあったのにWebpackerがインストールされてないんだっ、と思うところですが、
大人しく「rails webpacker:install」してみると

$ rails webpacker:install
Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/

「Yarnがインストールされていません」と言われてしまいました。
この為にrails newした時にWebpackerがインストールされなかったのでしょう。

Yarnとは

JavaScriptのパッケージマネージャのこと。
下記の記事がわかりやすかったです。
JavaScriptのパッケージマネージャーnpmとYarnについて解説します!

Yarnのインストール

$ brew install yarn

もう一度 rails webpacker:install をしてみる

$ rails webpacker:install
      create  config/webpacker.yml
Copying webpack core config
      create  config/webpack
      create  config/webpack/development.js
      create  config/webpack/environment.js
      create  config/webpack/production.js
      create  config/webpack/test.js
          ・
          ・
          ・
Webpacker successfully installed ? ?

無事にWebpackerをインストールできました。

あらためて rails s してみる

スクリーンショット 2020-09-10 22.57.21.png
エラー回避できました。

参考にさせていただきました

Rails6 Webpackerでエラーが出た

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

Postgresエラー「PG::ConnectionBad」

 何度もつまづいたので一旦簡単にメモ

PG::ConnectionBad

could not connect to server: Connection refused Is the server running on host "localhost" (::1) and accepting TCP/IP connections on port 5432? could not connect to server: Connection refused Is the server running on host "localhost" (127.0.0.1) and accepting TCP/IP connections on port 5432?

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

% postgres -D /usr/local/var/postgres
2020-09-10 22:13:34.167 JST [8631] FATAL:  lock file "postmaster.pid" already exists
2020-09-10 22:13:34.167 JST [8631] HINT:  Is another postmaster (PID 494) running in data directory "/usr/local/var/postgres"?
% rm /usr/local/var/postgres/postmaster.pid

rmでpostmaster.pidを削除
  

% brew services restart postgresql

リスタートしてあげる。

  

% rails s

起動して確認

ポイント

postmaster.pidが残っていること
正常にpostgresqlが終わらせられていないと
postmaster.pidというファイルが残ってしまった結果
接続できない起動できないエラーが発生するみたいです。

もう少し詳しく調べて再度更新予定。
postgresqlの起動と停止についての仕組みをしる必要がありそう
今回はメモまで。

参考記事

https://qiita.com/great084/items/98c83364f246473249c4

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

Railsでmechanizeを使ってスクレイピング

PythonでWebスクレイピングをやったことありましたが、
Railsもできるんだよねと思い立って、アプリ作る際に導入してみたい!と思いやってみたの記録になります。

今回は、その日の平均株価とかをスクレイピングします。

参考にさせていただいた記事
https://qiita.com/soehina/items/948f7f158a3a2d5be1dc

下準備

Railsアプリの作成
gem 'mechanize'のインストール

View

index.html.erb
  <%= @values.join %>

Controller

_controller.rb
  require 'mechanize'

  def index
    @scrps = Scrp.all
    agent = Mechanize.new
    @values = []
    page = agent.get("https://www.nikkei.com/markets/worldidx/chart/nk225/")
    @elements=  page.search('li.m-trend_economic_table_list')
    @elements.each do |element|
      @values << element.inner_text
    end
  end

getメソッドとsearchメソッドで、情報を取得し
inner_textで文字を引っこ抜いてくれます。
それをvaluesに入れてあげて、配列にしています。

とても簡単に、スクレイピングができました。
お疲れ様です。おわり!

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

form.labelで文字をエスケープしたときの実装メモ

<%= form.label :title, "件名", for: "title" %>
<%= form.text_field :title, id: "title", placeholder: "件名を入力してください" %>

上記のlabelの中の件名を<span>件名<span>としたい。(bootstrapをあてる関係で)
しかし

<%= form.label :title,"<span>件名</span>", for: "title" %>

としても意図した挙動にはならない。
これはhtmlタグが文字列として認識されてしまうから。
なので"件名"の部分だけエスケープさせたい。
エスケープで検索すると、html_safe、raw、sanitize等いくつかメソッドがあるとのこと。

(参考)
https://madogiwa0124.hatenablog.com/entry/2017/09/16/105843
https://railsguides.jp/action_view_overview.html#sanitizehelper

Railsガイドより、

html_safeやrawではなく、後述の#sanitizeメソッドを使うことが推奨されています。セキュリティ上の問題が生じるため、ユーザー入力に対して#html_safeや#rawを使ってはいけません。

とあることから
ユーザー情報を入力するためのinputタグなどにはsanitizeメソッドを、ユーザー情報以外であればhtml_safeやrawを使うっぽい。

ユーザー情報入力フォーム → sanitize
ユーザー情報以外の入力フォーム → raw
入力フォーム以外 → html_safe

こんな感じ??

実際のオプションの指定の仕方はhttps://api.rubyonrails.org/を参考に、以下のように実装したら期待した表示に。

<%= form.label :title, '<span>件名</span>'.html_safe, for: "title" %>
<%= form.text_field :title, id: "title", placeholder: "件名を入力してください" %>

ちなみに

https://api.rubyonrails.org/
でlabelを検索すると
ActionView::Helpers::FormBuilderのlabelとActionView::Helpers::FormHelperのlabelが引っかかった。
この違いって何???と悩んだのでメモ

FormHelperはform_tag
FormBuilderはform_fom、form_with

今回はレシーバがフォームヘルパオブジェクトじゃなくてフォームビルダオブジェクトになるので、FormBuilderのlabelのオプション指定を調べて実装する必要があった。

レシーバとは

obj.method

こういう記載があるところの

obj(レシーバ).method(メソッド)

この前の部分。

aaa.label_tag("hoge".html_safe)
上記だとこのaaaの部分。

今回だとform_withで実装しているため、form.labelのレシーバはFormBuilder

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

プロフィール編集機能の実装

備忘録です!!

新たなカラムの追加

userのアバター画像のカラムを生成します。

rails g migration add_avatar_to_users avatar:string

viewの生成

プロフィール詳細画面として

profiles/show.html.erb
<% content_for(:title, 'プロフィール') %>
<div class="container pt-3">
  <div class="row">
    <div class="col-md-10 offset-md-1 mb-5">
      <h1 class="float-left"><%= t('.title') %></h1>
      <%=link_to "編集", edit_profile_path, class: 'btn btn-success float-right' %>
    </div>
    <div class="col-md-10 offset-md-1">
      <table class="table">
        <tbody>
          <tr>
            <th>
              <span>メールアドレス</span>
            </th>
            <td><%= current_user.email %></td>
          </tr>
          <tr>
            <th>
              <span>氏名</span>
            </th>
            <td><%= current_user.decorate.full_name %></td>
          </tr>
          <tr>
            <th>
              <span>アバター</span>
            </th>
            <td><%= image_tag current_user.avatar.url, size: '50x50', class: 'rounded-circle mr15' %></td>
          </tr>

        </tbody>
      </table>
    </div>
  </div>
</div>

プロフィール変更画面

edit.html.erb
<% content_for(:title, 'プロフィール編集') %>
<div class="container pt-3">
  <div class="row">
    <div class="col-md-10 offset-md-1 mb-5">
      <h1 class="float-left"><%= t('.title') %></h1>
    </div>
    <div class="col-md-10 offset-md-1">
      <%= form_with model: @user, url: profile_path, local: true do |f| %>
        <%= render "shared/error_messages", object: f.object %>
        <div class="form-group">
          <%= f.label :email %>
          <%= f.email_field :email, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.label :last_name %>
          <%= f.text_field :last_name, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.label :first_name %>
          <%= f.text_field :first_name, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.label :avatar %>
          <%= f.file_field :avatar, class: "form-control", accept: 'image/*', onchange: 'previewFileWithId(preview)' %>
          <%= f.hidden_field :avatar_cache %>
          <div class='mt-3 mb-3'>
            <%= image_tag @user.avatar.url , size: '100x100', class: 'rounded-circle' %>
          </div>
        </div>
        <%= f.submit (t 'defaults.update'), class: "btn btn-primary"%>
      <% end %>  
    </div>
  </div>
</div>

ルーティングの設定

今回は、resourcesではなく、resourceを使っていきます。

本来、/users/:id/edit、のようになるはずですが、もしidを変えれば、他のユーザーのプロフィール編集ページに入ることができてしまう可能性があります。(もちろん、入れないようにバリデーションはかけますが)

プロフィール変更ページは自分自身に対してしか使わないので、idを用いず、pofile/editとなるようにルーティングを設定していきます。

resource resource :profile, only: %i[show edit update]

profileコントローラーの生成

rails g controller profiles

userの情報は、userモデルに紐づいたデータベースに保存されていますが、今回は新たにモデルに紐づかないprofileコントローラーを生成し、profileコントローラーでプロフィールの編集ページを実装していきます。

profile_controller.rb
def edit
  @user = User.find(current_user.id)
end

def update
  @user = User.find(current_user.id)
  if @user.update(user_params)
    redirect_to profile_path, success: t('defaults.message.edited', item: User.model_name.human)
  else
    flash.now['danger'] = t('defaults.message.not_edited', item: User.model_name.human)
    render :edit 
  end
end

private

def user_params
  params.require(:user).permit(:email, :first_name, :last_name, :avatar)
end

実装完了です!

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

renderのcollectionオプションを使った繰り返し処理の省略

動作環境
Ruby 2.6.5
Rails 6.0.3.2

collectionを使うことでrenderで呼び出した部分テンプレートを繰り返し処理する場合は、省略して記述できることを学んだので、投稿してみました。

collectionを使わない場合

index.html.erb
<% @hoges.each do |hoge| %>
  <%= render partial: "huga", locals: {hoge: hoge} %>
<% end %>

上記のコードは、部分テンプレートhugaを呼び出し、その中でhogeという変数を渡して繰り返し処理を行うというコードです。
これをcollectionを使うとどうなるのかを見てみましょう。

collectionを使う場合

index.html.erb
<%= render partial: "huga", collection: @hoges %>

こちらのコードは先ほどのコードと全く同じ意味を持ちます。3行のコードが1行で済むので、非常に楽に記述できることがわかると思います。
注意していただきたいのが、部分テンプレートhugaで@hoge@hogesと記述してしまうとエラーが発生してしまうことです。collectionの後の@hogesはコントローラーから受け取っているインスタンス変数です。実際に、部分テンプレートに渡している変数は先ほどのコードと同じhogeですので気をつけてください。

急にrenderのオプションでcollectionが出てきて、私は混乱してしまったことがあるので、この記事が少しでも役に立てればと思います。

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

【Appleサブスクリプションオファー】プロモーションオファー署名の作成方法

tl;dr

Appleのサブスクリプションオファーのプロモーションオファーに関しての情報がなかった。
これが、日本初の資料。

プロモーションオファーとは

WWDC 2019動画

https://developer.apple.com/videos/play/wwdc2019/305

Apple公式ドキュメント

https://developer.apple.com/jp/app-store/subscriptions/#subscription-offers

7ec9cdbd-1f10-4e9a-a5a5-c76e2c99f9ee.png

対象となるのは、そのサブスクリプションを現在利用している、または過去に利用したことがあるお客様です。
これらのオファーによって、ユーザー数の拡大や維持のため、
独自のプロモーションを柔軟に行うことができるようになります。
キャンペーンを通じて、サブスクリプションをキャンセルした利用者に再サブスクリプションを促したり、
別のサブスクリプションへのアップグレードを特別価格で提供したりすることができます。

準備

Apple公式ドキュメント

サブスクリプションオファーの設定

https://developer.apple.com/jp/documentation/storekit/in-app_purchase/setting_up_subscription_offers/

実装

※今回は、サーバーサイドでの署名作成に関してのみ記載する

Apple公式ドキュメント

プロモーションオファー用の署名の生成
https://developer.apple.com/jp/documentation/storekit/in-app_purchase/generating_a_signature_for_subscription_offers/

署名の生成に必要なもの

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

■appBundleID

環境変数で持つ

■keyIdentifier

環境変数で持つ

■productIdentifier

アプリ側からパラメータでもらう

■offerIdentifier

アプリ側からパラメータでもらう

■applicationUsername

アプリ側からパラメータでもらう

■nonce

サーバー側で生成する

■timestamp

サーバー側で生成する

署名

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

署名の生成

サンプルコード
本来はメソッドで分割するが、わかりやすさを重視する

require 'openssl'
require 'base64'
require 'securerandom'
require 'json'

# 環境変数として読み込むが、あえて記載する
private_key = '-----BEGIN PRIVATE KEY-----xxxxxxxxxxxxxxxxxxx-----END PRIVATE KEY-----'

# 環境変数から読み込んだ秘密鍵の改行コードがエスケープされてしまうのを防ぐ
private_key = OpenSSL::PKey::EC.new(private_key.gsub(/\\n/, "\n")))

app_bundle_id = 'xxxx'
key_identifier = 'xxxx'
product_identifier = 'xxxx'
offer_identifier = 'xxxx'
application_username = 'xxxx'
nonce = SecureRandom.uuid
timestamp = (Time.current.to_f * 1000).to_i.to_s

# 不可視の分離文字('\u2063')をパラメータの間にはさみ、結合する

payload = app_bundle_id + "\u{2063}" +
          key_identifier + "\u{2063}" +
          product_identifier + "\u{2063}" +
          offer_identifier + "\u{2063}" +
          application_username + "\u{2063}" +
          nonce + "\u{2063}" +
          timestamp

# 署名

# Ruby2.4.0以降であれば
# signature = private_key.sign(digest, data)

# SHA-256ハッシュを使って署名
signature = private_key.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(payload))

# base64にエンコード
# strict_encode64を使い、改行コードを消す
signature_base64 = Base64.strict_encode64(signature)


# 検証

# OpenSSL::PKey::ECオブジェクトを生成
ec = OpenSSL::PKey::EC.new(private_key.group)
ec.public_key = private_key.public_key

# SHA-256ハッシュで検証
digest = OpenSSL::Digest::SHA256.new

# payloadを秘密鍵で署名したその署名文字列がsignatureであることを公開鍵を使って検証
ec.verify(digest, signature, payload)

result = { key_identifier: key_identifier, nonce: nonce, timestamp: timestamp, signature: signature_base64 }.to_json

楕円曲線デジタル署名アルゴリズム(ECDSA)について

最初はruby_ecdsaというgemを使おうかと検討していた
https://github.com/DavidEGrayson/ruby_ecdsa

require 'ecdsa'
require 'securerandom'
require 'digest/sha2'

group = ECDSA::Group::Secp256k1

private_key = 1 + SecureRandom.random_number(group.order - 1)
public_key = group.generator.multiply_by_scalar(private_key)

message = 'ECDSA is cool.'
digest = Digest::SHA2.digest(message)

temp_key = 1 + SecureRandom.random_number(group.order - 1)
signature = ECDSA.sign(group, private_key, digest, temp_key)

valid = ECDSA.valid_signature?(public_key, digest, signature)
puts "valid: #{valid}"

が、秘密鍵が数値を想定したつくりになっていたので使用を見送った

Apple公式の署名作成サンプル

JavaScriptとNode.jsを使ったサンプル

https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/generating_a_subscription_offer_signature_on_the_server

サーバを起動して、アクセスするとレスポンスが返る

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

Appleのサブスクリプションオファーのプロモーションオファー署名作成

tl;dr

Appleのサブスクリプションオファーのプロモーションオファーに関しての情報がなかった。
これが、日本初の資料。

プロモーションオファーとは

WWDC 2019動画

https://developer.apple.com/videos/play/wwdc2019/305

Apple公式ドキュメント

https://developer.apple.com/jp/app-store/subscriptions/#subscription-offers

7ec9cdbd-1f10-4e9a-a5a5-c76e2c99f9ee.png

対象となるのは、そのサブスクリプションを現在利用している、または過去に利用したことがあるお客様です。
これらのオファーによって、ユーザー数の拡大や維持のため、
独自のプロモーションを柔軟に行うことができるようになります。
キャンペーンを通じて、サブスクリプションをキャンセルした利用者に再サブスクリプションを促したり、
別のサブスクリプションへのアップグレードを特別価格で提供したりすることができます。

準備

Apple公式ドキュメント

サブスクリプションオファーの設定

https://developer.apple.com/jp/documentation/storekit/in-app_purchase/setting_up_subscription_offers/

実装

※今回は、サーバーサイドでの署名作成に関してのみ記載します

Apple公式ドキュメント

プロモーションオファー用の署名の生成
https://developer.apple.com/jp/documentation/storekit/in-app_purchase/generating_a_signature_for_subscription_offers/

署名の生成に必要なもの

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

■appBundleID

環境変数で持つ

■keyIdentifier

環境変数で持つ

■productIdentifier

アプリ側からパラメータでもらう

■offerIdentifier

アプリ側からパラメータでもらう

■applicationUsername

アプリ側からパラメータでもらう

■nonce

サーバー側で生成する

■timestamp

サーバー側で生成する

署名

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

署名の生成

サンプルコード
本来はメソッドで分割するが、わかりやすさを重視する

require 'openssl'
require 'base64'
require 'securerandom'
require 'json'

# 環境変数として読み込むが、あえて記載する
private_key = '-----BEGIN PRIVATE KEY-----xxxxxxxxxxxxxxxxxxx-----END PRIVATE KEY-----'

# 環境変数から読み込んだ秘密鍵の改行コードがエスケープされてしまうのを防ぐ
private_key = OpenSSL::PKey::EC.new(private_key.gsub(/\\n/, "\n")))

app_bundle_id = 'xxxx'
key_identifier = 'xxxx'
product_identifier = 'xxxx'
offer_identifier = 'xxxx'
application_username = 'xxxx'
nonce = SecureRandom.uuid
timestamp = (Time.current.to_f * 1000).to_i.to_s

# 不可視の分離文字('\u2063')をパラメータの間にはさみ、結合する

payload = app_bundle_id + "\u{2063}" +
          key_identifier + "\u{2063}" +
          product_identifier + "\u{2063}" +
          offer_identifier + "\u{2063}" +
          application_username + "\u{2063}" +
          nonce + "\u{2063}" +
          timestamp

# 署名

# Ruby2.4.0以降であれば
# signature = private_key.sign(digest, data)

# SHA-256ハッシュを使って署名
signature = private_key.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(payload))

# base64にエンコード
# strict_encode64を使い、改行コードを消す
signature_base64 = Base64.strict_encode64(signature)


# 検証

# OpenSSL::PKey::ECオブジェクトを生成
ec = OpenSSL::PKey::EC.new(private_key.group)
ec.public_key = private_key.public_key

# SHA-256ハッシュで検証
digest = OpenSSL::Digest::SHA256.new

# payloadを秘密鍵で署名したその署名文字列がsignatureであることを公開鍵を使って検証
ec.verify(digest, signature, payload)

result = { key_identifier: key_identifier, nonce: nonce, timestamp: timestamp, signature: signature_base64 }.to_json

楕円曲線デジタル署名アルゴリズム(ECDSA)について

最初はruby_ecdsaというgemを使おうかと検討していた
https://github.com/DavidEGrayson/ruby_ecdsa

require 'ecdsa'
require 'securerandom'
require 'digest/sha2'

group = ECDSA::Group::Secp256k1

private_key = 1 + SecureRandom.random_number(group.order - 1)
public_key = group.generator.multiply_by_scalar(private_key)

message = 'ECDSA is cool.'
digest = Digest::SHA2.digest(message)

temp_key = 1 + SecureRandom.random_number(group.order - 1)
signature = ECDSA.sign(group, private_key, digest, temp_key)

valid = ECDSA.valid_signature?(public_key, digest, signature)
puts "valid: #{valid}"

が、秘密鍵が数値を想定したつくりになっていたので使用を見送った

Apple公式の署名作成サンプル

JavaScriptとNode.jsを使ったサンプル

https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/generating_a_subscription_offer_signature_on_the_server

サーバを起動して、アクセスするとレスポンスが返ります

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

Appleのサブスクリプションオファーのプロモーションオファー署名の作成方法

tl;dr

Appleのサブスクリプションオファーのプロモーションオファーに関しての情報がなかった。
これが、日本初の資料。

プロモーションオファーとは

WWDC 2019動画

https://developer.apple.com/videos/play/wwdc2019/305

Apple公式ドキュメント

https://developer.apple.com/jp/app-store/subscriptions/#subscription-offers

7ec9cdbd-1f10-4e9a-a5a5-c76e2c99f9ee.png

対象となるのは、そのサブスクリプションを現在利用している、または過去に利用したことがあるお客様です。
これらのオファーによって、ユーザー数の拡大や維持のため、
独自のプロモーションを柔軟に行うことができるようになります。
キャンペーンを通じて、サブスクリプションをキャンセルした利用者に再サブスクリプションを促したり、
別のサブスクリプションへのアップグレードを特別価格で提供したりすることができます。

準備

Apple公式ドキュメント

サブスクリプションオファーの設定

https://developer.apple.com/jp/documentation/storekit/in-app_purchase/setting_up_subscription_offers/

実装

※今回は、サーバーサイドでの署名作成に関してのみ記載する

Apple公式ドキュメント

プロモーションオファー用の署名の生成
https://developer.apple.com/jp/documentation/storekit/in-app_purchase/generating_a_signature_for_subscription_offers/

署名の生成に必要なもの

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

■appBundleID

環境変数で持つ

■keyIdentifier

環境変数で持つ

■productIdentifier

アプリ側からパラメータでもらう

■offerIdentifier

アプリ側からパラメータでもらう

■applicationUsername

アプリ側からパラメータでもらう

■nonce

サーバー側で生成する

■timestamp

サーバー側で生成する

署名

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

署名の生成

サンプルコード
本来はメソッドで分割するが、わかりやすさを重視する

require 'openssl'
require 'base64'
require 'securerandom'
require 'json'

# 環境変数として読み込むが、あえて記載する
private_key = '-----BEGIN PRIVATE KEY-----xxxxxxxxxxxxxxxxxxx-----END PRIVATE KEY-----'

# 環境変数から読み込んだ秘密鍵の改行コードがエスケープされてしまうのを防ぐ
private_key = OpenSSL::PKey::EC.new(private_key.gsub(/\\n/, "\n")))

app_bundle_id = 'xxxx'
key_identifier = 'xxxx'
product_identifier = 'xxxx'
offer_identifier = 'xxxx'
application_username = 'xxxx'
nonce = SecureRandom.uuid
timestamp = (Time.current.to_f * 1000).to_i.to_s

# 不可視の分離文字('\u2063')をパラメータの間にはさみ、結合する

payload = app_bundle_id + "\u{2063}" +
          key_identifier + "\u{2063}" +
          product_identifier + "\u{2063}" +
          offer_identifier + "\u{2063}" +
          application_username + "\u{2063}" +
          nonce + "\u{2063}" +
          timestamp

# 署名

# Ruby2.4.0以降であれば
# signature = private_key.sign(digest, data)

# SHA-256ハッシュを使って署名
signature = private_key.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(payload))

# base64にエンコード
# strict_encode64を使い、改行コードを消す
signature_base64 = Base64.strict_encode64(signature)


# 検証

# OpenSSL::PKey::ECオブジェクトを生成
ec = OpenSSL::PKey::EC.new(private_key.group)
ec.public_key = private_key.public_key

# SHA-256ハッシュで検証
digest = OpenSSL::Digest::SHA256.new

# payloadを秘密鍵で署名したその署名文字列がsignatureであることを公開鍵を使って検証
ec.verify(digest, signature, payload)

result = { key_identifier: key_identifier, nonce: nonce, timestamp: timestamp, signature: signature_base64 }.to_json

楕円曲線デジタル署名アルゴリズム(ECDSA)について

最初はruby_ecdsaというgemを使おうかと検討していた
https://github.com/DavidEGrayson/ruby_ecdsa

require 'ecdsa'
require 'securerandom'
require 'digest/sha2'

group = ECDSA::Group::Secp256k1

private_key = 1 + SecureRandom.random_number(group.order - 1)
public_key = group.generator.multiply_by_scalar(private_key)

message = 'ECDSA is cool.'
digest = Digest::SHA2.digest(message)

temp_key = 1 + SecureRandom.random_number(group.order - 1)
signature = ECDSA.sign(group, private_key, digest, temp_key)

valid = ECDSA.valid_signature?(public_key, digest, signature)
puts "valid: #{valid}"

が、秘密鍵が数値を想定したつくりになっていたので使用を見送った

Apple公式の署名作成サンプル

JavaScriptとNode.jsを使ったサンプル

https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/generating_a_subscription_offer_signature_on_the_server

サーバを起動して、アクセスするとレスポンスが返る

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

Ruby on Railsの開発環境でPostgreSQLを利用してPJを作成する

Rails new

$ rails new app-name -d postgresql
$ cd app-name

app-nameは任意の名前をつける

vs codeを開く

$ code .

この時点で

-bash: code: command not found

と怒られた方はvscodeの設定が必要

コマンドパレットを開き「shell command」
すると『Shell Command: Install 'code' command in PATH command.』
が表示されるので適用するとできるようになる

データベースの作成

$ rails db:create

localhost:3000にアクセスするとRailsの初期画面が表示されたのでできているのではないかと思います。

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

(ギリ)20代の地方公務員がRailsチュートリアルに取り組みます【第6章】

前提

・Railsチュートリアルは第4版
・今回の学習は3周目(9章以降は2周目)
・著者はProgate一通りやったぐらいの初学者

基本方針

・読んだら分かることは端折る。
・意味がわからない用語は調べてまとめる(記事最下段・用語集)。
・理解できない内容を掘り下げる。
・演習はすべて取り組む。
・コードコピペは極力しない。

続いて第6章入りまーす。こっから第12章まで、ログインと認証システムの開発に取り掛かるそうな。長丁場ですがやりきってやりましょう。

本日の一曲はこちら。
17歳とベルリンの壁 "プリズム"
ここ数年あんまり音楽開拓してなかったけど、良い国産シューゲバンドが出てきるな〜。

 

【6.1.1 データベースの移行 演習】

1. Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。これはデータベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われます。さて、あなたの環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル (リスト 6.2) の内容を比べてみてください。
→ 何が正解か分かんないけど、マイグレーションファイルの中身は反映されてますね。

 
2. ほぼすべてのマイグレーションは、元に戻すことが可能です (少なくとも本チュートリアルにおいてはすべてのマイグレーションを元に戻すことができます)。元に戻すことを「ロールバック (rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。

$ rails db:rollback

上のコマンドを実行後、db/schema.rbの内容を調べてみて、ロールバックが成功したかどうか確認してみてください (コラム 3.1ではマイグレーションに関する他のテクニックもまとめているので、参考にしてみてください)。上のコマンドでは、データベースからusersテーブルを削除するためにdrop_tableコマンドを内部で呼び出しています。これがうまくいくのは、drop_tableとcreate_tableがそれぞれ対応していることをchangeメソッドが知っているからです。この対応関係を知っているため、ロールバック用の逆方向のマイグレーションを簡単に実現することができるのです。なお、あるカラムを削除するような不可逆なマイグレーションの場合は、changeメソッドの代わりに、upとdownのメソッドを別々に定義する必要があります。詳細については、Railsガイドの「Active Record マイグレーション」を参照してください。
→ rails db:migrateを実行。スキーマの中身が消えてます。

schema.rb
ActiveRecord::Schema.define(version: 0) do

end

3. もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください。
→ 戻った!

 

【6.1.2 modelファイル 演習】

1. Railsコンソールを開き、User.newでUserクラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください (ヒント: 4.4.4で紹介したテクニックを使ってみてください)。
2. 同様にして、ApplicationRecordがActiveRecord::Baseを継承していることについて確認してみてください。
→ コンソール上で下記実行。

>> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> user.class
=> User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime)
>> user.class.superclass
=> ApplicationRecord(abstract)
>> user.class.superclass.superclass
=> ActiveRecord::Base

 

【6.1.3 ユーザーオブジェクトを作成する 演習】

1. user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。
→ あいよ。

>> user.name.class
=> String
>> user.email.class
=> String

 
2. created_atとupdated_atは、どのクラスのインスタンスでしょうか?
→ 両方ともActiveSupport::TimeWithZoneクラス(この記事が参考になりそう)

>> user.created_at.class
=> ActiveSupport::TimeWithZone
>> user.updated_at.class
=> ActiveSupport::TimeWithZone

 

【6.1.4 ユーザーオブジェクトを検索する 演習】

1. nameを使ってユーザーオブジェクトを検索してみてください。また、 find_by_nameメソッドが使えることも確認してみてください (古いRailsアプリケーションでは、古いタイプのfind_byをよく見かけることでしょう)
→ 下記

>> User.find_by(name: "shoji")
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "shoji"], ["LIMIT", 1]]
=> #<User id: 2, name: "shoji", email: "shoji@mail.com", created_at: "2020-09-08 22:54:09", updated_at: "2020-09-08 22:54:09">
>> User.find_by_name("shoji")
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "shoji"], ["LIMIT", 1]]
=> #<User id: 2, name: "shoji", email: "shoji@mail.com", created_at: "2020-09-08 22:54:09", updated_at: "2020-09-08 22:54:09">

 
2. 実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。
→ 下記(ついでに上位クラスまで調べています)

>> users = User.all
  User Load (0.2ms)  SELECT  "users".* FROM "users" LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, name: "miura", email: "miura@mail.com", created_at: "2020-09-08 22:53:59", updated_at: "2020-09-08 22:53:59">, #<User id: 2, name: "shoji", email: "shoji@mail.com", created_at: "2020-09-08 22:54:09", updated_at: "2020-09-08 22:54:09">]>
>> users.class
=> User::ActiveRecord_Relation
>> users.class.superclass
=> ActiveRecord::Relation
>> users.class.superclass.superclass
=> Object
>> users.class.superclass.superclass.superclass
=> BasicObject
>> users.class.superclass.superclass.superclass.superclass
=> nil

 
3. User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質があります。これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」。(訳注: そういえばRubyKaigi 2016の基調講演で、Ruby作者のMatzがダックタイピングについて説明していました。2〜3分の短くて分かりやすい説明なので、ぜひ視聴してみてください!)
→ データ数が表示されました。

>> User.all.length
  User Load (0.2ms)  SELECT "users".* FROM "users"
=> 2

 

【6.1.5 ユーザーオブジェクトを更新する 演習】

1. userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。
→ 下記

>> user1.name = "yongon"
=> "yongon"
>> user1.save
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.6ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "yongon"], ["updated_at", "2020-09-08 23:12:52.428275"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

 
2. 今度はupdate_attributesを使って、email属性を更新および保存してみてください。
→ 下記(ミスっていろいろやり直してるのidがずれてます)

>> user1.update_attributes(name: "yongon", email: "yongon@mail.com")
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.1ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["email", "yongon@mail.com"], ["updated_at", "2020-09-09 03:12:33.687572"], ["id", 4]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

 
3. 同様にして、マジックカラムであるcreated_atも直接更新できることを確認してみてください。ヒント: 更新するときは「1.year.ago」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。
→ 下記

>> user1.update_attribute(:created_at, 1.year.ago)
   (0.1ms)  SAVEPOINT active_record_1
  SQL (1.1ms)  UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["created_at", "2019-09-09 03:18:24.829284"], ["updated_at", "2020-09-09 03:18:24.830017"], ["id", 4]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

 

【6.2.1 有効性を検証する メモと演習】

 setupメソッドを使うと、メソッド内に書かれた処理がテスト直前に実行される。この中でインスタンス変数を定義しておけば、すべてのテスト内で使えるようになる。

1. コンソールから、新しく生成したuserオブジェクトが有効 (valid) であることを確認してみましょう。
→ 下記

>> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> user.valid?
=> true

2. 6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。
→ 一回コンソール閉じてるから消えてるよ〜。どうせ有効なので割愛。

 

【6.2.2 存在性を検証する メモと演習】

 assert_not @user.valid? がRED
→ 「@userは有効ちゃうよな?」と主張してるのに、「有効やんけ!」とツッコミが入ってる状態 と考えると分かりやすい。
 validatesにも出てくるが、メソッドの最後の引数としてハッシュを渡す場合、{ }は省略可能

1. 新しいユーザーuを作成し、作成した時点では有効ではない (invalid) ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。
→ nameもemailも入力してないからヴァリデーションが働いてます。

>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.valid?
=> false
>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]

  
2. u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?
→ このページに書いてました。.messagesつけてもつけなくても一緒ですね。

>> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
>> u.errors[:email]
=> ["can't be blank"]
>> u.errors.messages[:email]
=> ["can't be blank"]

 

【6.2.3 長さを検証する 演習】

1. 長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。
2. 長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。
→ まとめて下記

>> user = User.new(name: "a"*55, email: "e"*244 + "@example.com")
=> #<User id: nil, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", email: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee...", created_at: nil, updated_at: nil>
>> user.valid?=> false
>> user.errors.full_messages=> ["Name is too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]

 

【6.2.4 フォーマットを検証する 演習】

 正規表現は覚える必要あるでしょうか?ややこしいので、都度調べて実装する方がいいような。
 
1. リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
→ たしかめるだけ。

 
2. 先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。
→ これもやるだけー。
 
3. foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
→ 確認しました。

 

【6.2.5 一意性を検証する メモと演習】

 ここの内容ちょっとややこしいけど、要はメールアドレスに関して大文字・小文字を区別しないように設定してると。テストは大文字ユーザーが有効ででない(大文字でも同じアドレス)ことを確かめてるわけか。
 そして、メールアドレスがデータベースに保存される前に、すべてを小文字にするためにコールバックメソッドが登場しました。軽く調べたところ、コールバックの利用は慎重にしないといけないようです。コールバックの中で条件分岐とか避けた方がいいみたい。

1. リスト 6.33のように、メールアドレスを小文字にするテストをリスト 6.26に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして redになることを、また、コメントアウトを解除すると greenになることを確認してみましょう。
→ 指示通り実行。before_saveで保存前に小文字に変換しているので、コメントアウトするとREDに、解除するとGREENになります。

2. テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。
→ ここで出てくる「!」は、破壊的な処理を表します。つまり、メールアドレスが小文字変換されたままで維持されるということ。書き換えてもテストはGREENです。

 

【6.3.2 ユーザーがセキュアなパスワードを持つ 演習】

1. この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。 
2. なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。
→ まとめていくよ!パスワードが空白はダメだってよ。

>> user = User.new(name: "kote", email: "kote@mail.com")
=> #<User id: nil, name: "kote", email: "kote@mail.com", created_at: nil, updated_at: nil, password_digest: nil>
>> user.valid?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "kote@mail.com"], ["LIMIT", 1]]
=> false
>> user.errors.messages
=> {:password=>["can't be blank"]}

 

【6.3.3 パスワードの最小文字数 演習】

1. 有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。
2. 上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。
→ 今回もまとめていくよ!もちろんパスワードが短すぎると怒られます。ここでhas_secure_passwordが働いて、パスワードがハッシュ化されているのが分かりますね。

>> user = User.new(name: "kote", email: "kote@mail.com", password: "kotte")
=> #<User id: nil, name: "kote", email: "kote@mail.com", created_at: nil, updated_at: nil, password_digest: "$2a$10$7Svz/KnRoF7zab0PnhKFL.n/OsSltRvvREHECcmuq.D...">
>> user.valid?  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "kote@mail.com"], ["LIMIT", 1]]
=> false
>> user.errors.messages=> {:password=>["is too short (minimum is 6 characters)"]}

 

【6.3.4 ユーザーの作成と認証 演習】

1. コンソールを一度再起動して (userオブジェクトを消去して)、このセクションで作ったuserオブジェクトを検索してみてください。
→ 下記

>> user = User.find(1)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2020-09-10 02:37:56", updated_at: "2020-09-10 02:37:56", password_digest: "$2a$10$A5n.HFBigQfwnWVJZw2N0e4M9sxPaR8ndLZwqtZWYS7...">

 
2. オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?
→ 下記。saveメソッドだと全ての属性を更新しようとするから、パスワードの更新でエラー吐いてるみたい。

>> user.name = "meshino"
=> "meshino"
>> user.save
   (0.1ms)  begin transaction
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ?  [["email", "mhartl@example.com"], ["id", 1], ["LIMIT", 1]]
   (0.0ms)  rollback transaction
=> false
>> user.errors.messages
=> {:password=>["can't be blank", "is too short (minimum is 6 characters)"]}

 
3. 今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。
→ ということで、update_attributeを使って更新します。(ただし、さっきエラー吐いたuserを再利用しているので、name更新後も有効ではありません。パスワードの再設定が必要と思われます)

>> user.update_attribute(:name, "nakamura")
   (0.1ms)  begin transaction
  SQL (4.2ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "nakamura"], ["updated_at", "2020-09-10 03:07:11.190666"], ["id", 1]]
   (6.4ms)  commit transaction
=> true
>> user.valid?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ?  [["email", "mhartl@example.com"], ["id", 1], ["LIMIT", 1]]
=> false
>> user.errors.messages=> {:password=>["can't be blank", "is too short (minimum is 6 characters)"]}

 

第6章まとめ

・データベースを更新する時はつどつどマイグレーションを作成してマイグレートしよう。
・Active Recordは便利。Railsは下火になってきてるけどActive Recordは利点とどっかで書かれてた。
・バリデーションで無効な入力内容を設定して弾こう。
・正規表現は多分そういうもんなんだと認識する程度でよいと思う。
・データベースにインデックス追加で検索効率向上。一意性も保証。
・has_secure_passwordはgemで利用しているわけだから、他にも便利なgemでセキュアな処理が実装できるんかな。と思って検索したらやっぱり出てきました。今後学んでいこう。

 
 いろいろ気になることを寄り道して調べているので時間がかかります。でも絶対チュートリアルの内容だけでは通用しないと思うので、他の様々なことにも興味を持って吸収していきます。
 さて次!第7章はユーザー登録の実装です!sign up!

 

⇦ 第5章はこちら
学習にあたっての前提・著者ステータスはこちら
 

なんとなくイメージを掴む用語集

・assert_not
 notで否定してるので、まんま逆の意味。対象が真なら失敗、偽なら成功。

・Active Record コールバック
 何かの処理の前/後に呼び出すメソッドを設定できる。詳しくはRailsガイドへ。

・スタブ
 テスト時に用意する代用品。テスト対象の処理から呼び出される代用品がスタブ、テスト対象の処理を呼び出す代用品がドライバ。

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

Mysqlにデータベースを変更する

database.ymlの中身を以下のように書き換えてください。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /tmp/mysql.sock
development:
  <<: *default
  database: amazon_app_mysql_development
test:
  <<: *default
  database: amazon_app_mysql_test
production:
  <<: *default
  database: amazon_app_mysql_production
  username: amazon_app_mysql
  password: <%= ENV['AMAZON_APP_MYSQL_DATABASE_PASSWORD'] %>

sqliteをコメントアウトする

Gemfile
#gem 'sqlite3', '~> 1.4'

mysqlを追加する

Gemfile
gem 'mysql2', '>= 0.4.4'

mysql2を更新させ、反映させる

ターミナル
$ bundle update

DBを初期化

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

Rails Tutorial 第13章 演習13.4.1.2 paramsハッシュがなくなる

演習13.4.1.2 paramsハッシュがなくなる

ネットの他の方を見ると、params:ハッシュがなくなっている方が多かったです。一方テキストではなくなっていません。最近テキストが書き換えられたのかもしれません。

ネットの方の例

test/integration/microposts_interface_test.rb
 post microposts_path, micropost: { content: content, picture: picture }

テキスト

test/integration/microposts_interface_test.rb
      post microposts_path, params: { micropost:
                                      { content: content,
                                        picture: FILL_IN } }

type="input"とtype=inputのどちらでも動きました。

ページのソースを見たところ、"input"だったので""を入れました。ネットの他の方は"”ありとなしのどちらもありました。

test/integration/microposts_interface_test.rb
 test "micropost interface" do

    assert_select 'input[type="file"]'

演習13.3.5.1 「有効な送信」でエラーを起こさせたところ他のところでエラーになる

#    if @micropost.save
    if true
      flash[:succsess] = "Micropost created!"
      redirect_to root_url

saveをしないようにコメントアウトしたところ、その前の「無効な送信」のテストが
エラーになってしまいました。saveをしないので、error_explanationが出ないようです。

FAIL["test_micropost_interface", MicropostsInterfaceTest, 0.9583898989999398]
 test_micropost_interface#MicropostsInterfaceTest (0.96s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:17:in `block in <class:MicropostsInterfaceTest>'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]エラーStandardError: An error has occurred, all later migrations canceled: Column `外部キー名` on table `テーブル名` does not match column `id` on `テーブル名`の対処方法

エラー内容

$ rails db:migrate

上記コマンドを実行すると発生するエラー。ターミナルでのエラー表記は以下の通り。
image.png

エラー文一部抜粋.
Column `user_id` on table `items` does not match column `id` on `users`, which has type `bigint(20)`. To resolve this issue, change the type of the `user_id` column on `items` to be :bigint. (For example `t.bigint :user_id`).

エラー文の一部を翻訳してみると...。

エラー文翻訳.
テーブル `items` のカラム `user_id` が `users` のカラム `id` と一致しません。この問題を解決するには、`items` の `user_id` カラムの型を :bigint に変更します。(例えば `t.bigint :user_id`)。

今回はitemsテーブルが外部キーとして指定しているカラムが参照元と一致しませんよ!というエラーですね。

対処法(仮説)

結論、Railsでは外部キーを使用する際はreferences型を推奨しているので、bigint型を使用する必要はありません。

このエラーのポイントは参照できませんということなので、マイグレーションファイルの作成順に問題があると仮説できます。

マイグレーションファイルの作成順とは?

外部キーを使用するテーブル(references型を記述するテーブル)と参照されるテーブルには作成順によって参照できなくなる場合があり、今回のエラーは作成順序の誤りで発生しました。

作成順は、①参照される側のテーブル→②外部キーを使用するテーブル(references型を記述するテーブル)です。
対処法はマイグレーションファイルの作成日時を修正してあげれば解決できます。

対処法(仮設検証)

image.png
添付画像の数字部分を、参照される側のテーブルよりも外部キーを使用するテーブルの数字を大きくすれば解決します。

エラー分からは少し推測しづらいエラーですね。

今回の場合だと、create_items20200909000000なら20200909100000でいいです。
ちなみに、最初の4桁は西暦、次の4桁は月日です

最後に

今回のエラーはテーブル数が増えると発生しやすいエラーなのかなと思います。
ただ、対象法を知っていれば問題なく解決できると思いますので、参考にしてみてください!

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

[Ransack] ransackable_scopesには気を付けろ

結論

ransackable_scopesで実行するscopeに渡す引数が以下の値だったら
ArgumentError wrong number of arguments (given 0, expected 1)
が発生するから気を付けろ!!!

  • "true", "TRUE", "t", "T", 1, "1"
  • "false","FALSE","f","F",0,"0"

これらの値はそれぞれTrue, Falseに暗黙的に変換されるため、引数に渡せない。

解決策

config/initializers/ransack.rb
Ransack.configure do |config|
  config.sanitize_custom_scope_booleans = false
end

これを追記するだけで、上述の値全てを渡せるようになります1

渡せる値をカスタムしたいなら

諦めてください。
暗黙的に変換される値は Ransack::Constants::BOOLEAN_VALUES2 で定義されています。
この定数はfreezeされています。
公式によるとfreezeしたオブジェクトをunfreezeする方法は無いそうです。3

There is no way to unfreeze a frozen object.

変換される値のカスタマイズついてはかなり調べましたが、無理でした。。。
一応、「暗黙的に変換する値をカスタマイズできるようにしたよ!」って内容のPRがマージされていました。
これによると、以下のように変換する値をカスタマイズできるらしいです。

Ransack.configure do |config|
   config.truthy_values_to_convert_in_custom_scopes = ['TRUE', 'true', '1']
   config.falsey_values_to_convert_in_custom_scopes = ['FALSE', 'no way no how']
end

しかし、それ通りに記述しても以下のようなエラーが発生しました。

=> NoMethodError: undefined method `truthy_values_to_convert_in_custom_scopes' for Ransack:Module

ファッ!?!?!?!?!?!?!?!?!?!?!?!?
PRの実装内容をを確認しましたが、概要で書かれているような内容は実装されていませんでした。。。
誰かここ分かる方がいれば教えてください。。。

また、@t_oginoginさんがRansackを使わない解決策を提案してくれているので、皆さんの実装状況に合わせてご参考下さい。
https://qiita.com/t_oginogin/items/b45636d64c271ebc409c

参考文献

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

rails tutorial 第8章

はじめに

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

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

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

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

第8章 基本的なログイン機構

8.1.2 ログインフォーム

ログインフォームを作成時のscopeの働きについてわからなかったので調べました。

form_with(url: login_path, scope: :session, local: true)

参考
https://qiita.com/akilax/items/f36b13f377f7e442bc73

あまり深く考え過ぎずにパラメーターを渡す際に必要なname値のプレフィックスと考えるのが良さそうです。

ざっくりと自分なりにまとめると、Active Recodeを継承しているオブジェクトのフォームでは

<%= form_with(model: @user, local: true) do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name, class: 'form-control' %>
.
.
<% end %>

とし、入力された値(生成されたinputタグのname値) へのアクセスは

params[:user][:name]

となり、入力結果をuserハッシュに保存していました。

今回のsessionフォームにおいても同様で、パラメーターに入力結果の値を渡すために、scopeで指定したハッシュに入力結果を保存しているということだと思います。

8.1.4 フラッシュメッセージを表示する

ログイン失敗時にエラーメッセージを表示します。

app/controllers/sessions_controller.rb
#リスト 8.8: ログイン失敗時の処理を扱う(誤りあり)
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      flash[:danger] = 'Invalid email/password combination' # 本当は正しくない
      render 'new'
    end
  end

  def destroy
  end
end

実は上のコードのままでは、リクエストのフラッシュメッセージが一度表示されると消えずに残ってしまいます。リスト 7.27でリダイレクトを使ったときとは異なり、表示したテンプレートをrenderメソッドで強制的に再レンダリングしてもリクエストと見なされないため、リクエストのメッセージが消えません。例えばわざと無効な情報を入力して送信してエラーメッセージを表示してから、Homeページをクリックして移動すると、そこでもフラッシュメッセージが表示されたままになっています。(rails tutorial 8章より引用)


ここで、なぜリクエストと見なされなければメッセージが消えないのか、また、エラーメッセージ表示後にhomeページをクリックして移動する際にGETリクエストをしているから消えてもよいのでは?という疑問が生まれました。

この疑問を解消するため、まずは改めてrenderとredirect_toの違いについて調べてみた。

参考
https://qiita.com/january108/items/54143581ab1f03deefa1
この記事を読んでもやはりhomeページをクリックする時にリクエストしてるよな、、、という疑問が残りました。
その後も色々調べてようやく回答にたどり着きました。どうやらflashについての理解が足りなかったようです。

参考
https://pikawaka.com/rails/flash

つまりflashメッセージが表示されてから、リクエストを受け、アクションが実行されてからフラッシュメッセージが消去されるようです。

だからrenderメソッドでviewが表示された後、homeリクエストをしてもメッセージが残っているようでした。

試しに
1.無効な情報を入力して送信
2.エラーメッセージを確認
3.homeページに移動
4.homeページでもエラーメッセージ消えていないことを確認
5.再度homeページをクリック
とするとエラーメッセージは消えていました。

凄いすっきりしました!!

8.2 ログイン

Railsのセッション用ヘルパーはビューにも自動的に読み込まれます。Railsの全コントローラの親クラスであるApplicationコントローラにこのモジュールを読み込ませれば、どのコントローラでも使えるようになります 。(rails tutorial 8章より引用)

注意
viewには自動で読み込まれますが、コントローラーなどで、ヘルパーに設定したメソッドなどを利用する場合はincludeで読み込む必要があります。

8.2.2 現在のユーザー

このトピックではわかりにくいところがあり、初めてQiitaで質問を利用させていただきました。

内容を深堀したとき、なぜfindメソッドではなくfind_byメソッドを利用すべきだったのか、わかりませんでしたので質問をしました。

質問内容
https://qiita.com/shun_study_p/questions/da3de50fe7826dc151ed
回答をしていただいた方、ありがとうございます。


@current_user ||= User.find_by(id: session[:user_id])

Userオブジェクトそのものの論理値は常にtrueになることです。そのおかげで、@current_userに何も代入されていないときだけfind_by呼び出しが実行され、無駄なデータベースへの読み出しが行われなくなります。(rails tutorial 8章より引用)

ここの文についても自分なりに補足

@current_user ||= User.find_by(id: session[:user_id])

この一文は@current_userがnilの時はfind_byメソッドを実行しUserオブジェクトを作成するというもの。(@current_userにUserオブジェクトが代入される)
そうなった後は@current_userはUserオブジェクトとなり、またUserオブジェクトはtrueを返すので、以降は左辺がtrueとなり、無駄なfind_byメソッドは実行されず(@current_userがnilではないから)無駄なデータの呼び出しも行われなくなるということ。

8.2.3 レイアウトリンクを変更する

<%= link_to "Profile", current_user %>

復習も兼ねてこちらの一文に置いて何が行われているかというと

<%= link_to "Profile", "/users/#{current_user.id}" %>
<%= link_to "Profile", user_path(current_user.id) %>
<%= link_to "Profile", user_path(current_user) %>
<%= link_to "Profile", current_user %>

と省略されて行っています。

ここでまた一つの疑問が、
link_toの引数でモデルオブジェクトを渡すのはわかります。

でも今回渡してるのってメソッドじゃないの、、、?

こちらに関しては以下の記事を参考にしました。

参考記事1(7章でも参考にしました)
https://qiita.com/Kawanji01/items/96fff507ed2f75403ecb

参考記事2
https://teratail.com/questions/198096
どうやら戻り値にモデルのインスタンスを返すならそのメソッドはモデルオブジェクトとみなせるようですね。

8.2.4 レイアウトの変更をテストする

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

このdigestメソッドは、今後様々な場面で活用します。例えば9.1.1でもdigestを再利用するので、このdigestメソッドはUserモデル(user.rb)に置いておきましょう。この計算はユーザーごとに行う必要はないので、fixtureファイルなどでわざわざユーザーオブジェクトにアクセスする必然性はありません(つまり、インスタンスメソッドで定義する必要はありません)。(rails tutorial 8章より引用)


インスタンスメソッドとクラスメソッドの違いをあまり理解出来ていなかったのか、上の文の意味がよくわからない、、、

上の本文は

1.インスタンスメソッドで定義するとわざわざユーザーオブジェクトにアクセスする必要がある。
2.今回の"渡された文字列のハッシュを返すというdigestメソッド"はユーザーごとのインスタンスで行う必要はない。(クラスオブジェクトから直接実行すればよいもの)

と言っている。なんとなくだがこの2つが言いたいことはわかるような、、、

まだふんわりとした理解ですが、記事を参考に自分でまとめてみました。

参考
https://qiita.com/tbpgr/items/56eb65c0ea5882abbb07

つまり
クラスメソッドは○○クラス自身に関する情報の変更や参照の役割をもっている。

なので今回のようなパスワードのハッシュ化、記事の例にあるように男女といった性別の属性、これらはクラスメソッドであらかじめ定義しておくことができる。

インスタンスメソッドは、個別のインスタンスに関する情報の変更や参照の役割りを持っている。

なので特定のデータのパスワードや名前と言った情報の参照をしたいなどといったときはインスタンスメソッドを使う。(ユーザーオブジェクトにアクセスする必要があることもイメージしやすい)

こんなところだろうか、、、難しい、、

終わりに

今回の章も難しく、7割程度の理解で進んでしまった部分もあるので、また復習が必要だと思いました。

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

[Rails]1対1対多の場合のdelegateとhas_many-throughの挙動の違い

下記のように1対1対多の関係のモデルがあるとします。

  • UserとExamineeは1対1
  • ExamineeとTestは1対多
class User
  has_one :examinee
end

class Examinee
  belongs_to :user
  has_many :tests
end

class Test
  belongs_to :examinee
end

では、Userモデルから関連するTestモデルを取得したいときはどのように実装しますか?

様々なやり方がありますが、ActiveRecordの便利機能delegateを使うか、has_many-throughを使うことが多いのではないでしょうか?
どちらもやりたいことは達成できますが、発行されるクエリが少し違うので紹介します。

delegate

delegateを使うとメソッドを別クラスに委譲することが出来ます。
詳細はRailsガイドを参照してください。
3.4.1 delegate

今回の場合、下記のように実装します。

app/models/user.rb
delegate :tests, to: :examinee

実行すると下記の通り2つのクエリーが発行されます。
まず委譲先のexamineeを取得(1つ目のクエリー)して、その後、examinee.testsを実行(2つ目のクエリー)する挙動になっています。

irb> user.tests
Examinee Load SELECT `examinees`.* FROM `examinees` WHERE `examinees`.`user_id` = 1 LIMIT 1
Test Load SELECT `tests`.* FROM `tests` WHERE `tests`.`examinee_id` = 1

has_many-through

has_many-throughは多対多の時に使われることが多いですが、今回のように1対多の場合も利用できます。

詳細はRailsガイドを参照してください。
2.4 has_many :through 関連付け

今回の場合、下記のように実装します。

app/models/user.rb
has_many :tests, through: :examinee

実行すると下記の通り1つのクエリーが発行されます。
こちらの場合はjoinしたクエリーが1つだけ発行されます。
この機能が多対多に対応するように実装されていると考えると、deletgateのように2段階では効率よく取得できないのでjoinで取得しているんだなと理解できると思います。

irb> user.tests
Test Load SELECT `tests`.* FROM `tests` INNER JOIN `examinees` ON `tests`.`examinee_id` = `examinees`.`id` WHERE `examinees`.`user_id` = 1

最後に

2クエリーで取得するほうが良いのか、joinされた1クエリーで取得するほうが良いのかは実行環境によるので一概に良し悪しは判断出来ません。
というか大抵の場合はどちらで書いても問題なく動作するのでぶっちゃけどちらでもよいと思います。

ただ、ブラックボックス的に見ると同じことをしているように見えても、今回のように内部で発行されるクエリーが違ったりします。
たまにはこういう細かな違いを機能の成り立ちや目的などを考えならが確認してみると面白いと思います。

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

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

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

controllerを作る

前回はmodelを作ったので、今回はcontrollerを実装していきます。

$ rails g controller v1/posts

実行するとcontrollerとrequest specファイルが生成されます。

とりあえずcontrollerを以下まで実装します。

app/controllers/v1/posts_controller.rb
# frozen_string_literal: true

module V1
  #
  #  post controller
  #
  class PostsController < ApplicationController
    before_action :set_post, only: %i[show update destroy]

    def index
      # TODO
    end

    def show
      # TODO
    end

    def create
      # TODO
    end

    def update
      # TODO
    end

    def destroy
      # TODO
    end

    private

    def set_post
      @post = Post.find(params[:id])
    end

    def post_params
      params.permit(:subject, :body)
    end
  end
end
  • index: post一覧を取得する
  • show: post1レコードの情報を取得する(R)
  • create: post1レコードを作成する(C)
  • update:post1レコードを更新する(U)
  • destroy: post1レコードを削除する(D)

CRUDに沿ったcontrollerを、一旦ロジック無しで作ります。
なお、V1というnamespaceを切っているのはAPI開発ではよくやる手法です。
これにより後方互換の無いversion2を作る際、分離して開発がしやすくなります。

続いてroutesを設定します。

config/routes.rb
 # frozen_string_literal: true

 Rails.application.routes.draw do
+   namespace "v1" do
+     resources :posts
+   end
 end

これでCRUDのroutesが設定されます。確認してみましょう。

$ rails routes
...
                               Prefix Verb   URI Pattern                                                                              Controller#Action
                             v1_posts GET    /v1/posts(.:format)                                                                      v1/posts#index
                                      POST   /v1/posts(.:format)                                                                      v1/posts#create
                              v1_post GET    /v1/posts/:id(.:format)                                                                  v1/posts#show
                                      PATCH  /v1/posts/:id(.:format)                                                                  v1/posts#update
                                      PUT    /v1/posts/:id(.:format)                                                                  v1/posts#update
                                      DELETE /v1/posts/:id(.:format)                                                                  v1/posts#destroy
...

indexテストの実装

例によってテストを先に実装します。
挙動としては

  • 登録されたpostを返す
  • created_at降順でソート
  • 20件でlimit

でいきます。
簡易的なテストアプリケーションのチュートリアルのためpagerは組み込みませんが、もしかしたら今後記事を書くかもしれません。

spec/requests/v1/posts_controller.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe "V1::Posts", type: :request do
  describe "GET /v1/posts#index" do
    before do
      create_list(:post, 3)
    end
    it "正常レスポンスコードが返ってくる" do
      get v1_posts_url
      expect(response.status).to eq 200
    end
    it "件数が正しく返ってくる" do
      get v1_posts_url
      json = JSON.parse(response.body)
      expect(json["posts"].length).to eq(3)
    end
    it "id降順にレスポンスが返ってくる" do
      get v1_posts_url
      json = JSON.parse(response.body)
      first_id = json["posts"][0]["id"]
      expect(json["posts"][1]["id"]).to eq(first_id - 1)
      expect(json["posts"][2]["id"]).to eq(first_id - 2)
    end
  end
end
  • beforeは、同ブロック以下itで毎回事前に実行されます。
  • create_list(:post, 3)はpostを3レコード生成しDBに保存する処理です。
  • また、itブロック終了時にテストDBはrollbackされます。

つまり挙動をまとめると、
LINE 10:it "正常レスポンスコードが返ってくる"ブロック開始
LINE 8:3件分のpostが保存される
LINE 11:v1_posts_url(v1/posts/index)にgetリクエストを行う
LINE 12:レスポンスコードが:ok(200 正常)
LINE 13:it "正常レスポンスコードが返ってくる"ブロック終了。rollbackされpostレコードは0件に
LINE 14:it "件数が正しく返ってくる"ブロック開始
LINE 8:3件分のpostが保存される
LINE 15:v1_posts_url(v1/posts/index)にgetリクエストを行う
LINE 16:response.bodyをJSON.parseしてRubyの配列に変換
LINE 17:レスポンスのpostが3レコード
LINE 18:it "件数が正しく返ってくる"ブロック終了。rollbackされpostレコードは0件に

...

となります。

この時点ではcontroller未実装なので、当然テストはコケます。

なお、最後のテストは厳密にはcreated_atの比較が必要ですが、簡易的にidで比較をしています。
本来はlimitのテストもすべきですが省略します。興味があれば、create_listで21件作って20件しか返ってこないことを確認するテストを実装してみてください。

Tips.

ついでによく使うfactoryBotのメソッドを紹介しておきます。

  • build(:post) postを1レコード、メモリ上に生成。saveしない限りDBには反映されない。Post.newに相当
  • create(:post) postを1レコード生成しDBに保存。Post.create!に相当
  • build_list(:post, 5) postを5レコード生成。buildの複数版
  • create_list(:post, 5) postを5レコード生成。createの複数版

example.comをhostsに追加

なお、requestsテストは以下対応が必要です。

config/application.rb
...
     config.hosts << ".amazonaws.com"
+    config.hosts << "www.example.com"
...

なぜならrspecのテストはwww.example.comからのリクエストとして認識されるためです。

indexの実装

app/controllers/v1/posts_controller.rb
...
     def index
-      # TODO
+      posts = Post.order(created_at: :desc).limit(20)
+      render json: { posts: posts }
     end
...

これで一覧取得ができます。
試しにcurlでAPIを叩いてみます。

$ curl localhost:8080/v1/posts
{"posts":[{"id":2,"subject":"","body":"hoge","created_at":"2020-09-06T01:07:52.628Z","updated_at":"2020-09-06T01:07:52.628Z"},{"id":1,"subject":"hoge","body":"fuga","created_at":"2020-09-05T13:50:01.797Z","updated_at":"2020-09-05T13:50:01.797Z"}]}

もし空のdataが返ってきた場合は、rails cからpostのレコードを生成してみてください。

ここまでできたら、rubocopやrspec実行を忘れずに行った後、git commitしましょう。

続き

Rails 6で認証認可入り掲示板APIを構築する #6 show, create実装

連載目次へ

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

Railsのcsvダウンロードで直面する数々の問題を解決したらgemができた 〜csb gemの紹介〜

はじめに: csvダウンロードの処理をちゃんと書くのって意外と面倒じゃないですか?

例えば「rails csv ダウンロード」で検索すると以下のようなサンプルコードがよく出てきます。

posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end
index.csv.ruby
require 'csv'

CSV.generate do |csv|
  column_names = %w[投稿日 カテゴリ タイトル 本文]
  csv << column_names
  @posts.each do |post|
    column_values = [
      l(post.created_at.to_date),
      post.category.name,
      post.title,
      post.content,
    ]
    csv << column_values
  end
end

しかしこのサンプルコードでは次のような問題が解決出来ていません。

  • Excelで開くと文字化けする
  • レコード件数が大量にあった場合にメモリエラーやタイムアウトエラーが発生する可能性がある
  • 列名と値の定義が離れているために、カラム数が増えてくると可読性が悪く保守性が低くなりがち
  • CSV出力の条件が複雑化した場合にテストが書きにくい(system testで頑張るしかない)

この問題を解決するために、csbというgemを作りました。
このgemは弊社ソニックガーデンの複数のプロジェクトで1年以上、本番利用されてきた実績があります。

今回の記事ではこのgemの概要と使い方を紹介します。

何が出来るの?

  • BOM付きUTF-8で出力することで、Excelでも文字化けせずに開ける
  • 数十万件以上といった大量データの場合でも、ストリーミングダウンロードにすることでメモリエラーやタイムアウトエラーといったよく起こりがちなトラブルを防げる
  • 可読性高くメンテナブルにエクスポート用の処理を書ける
  • エクスポート用の処理を切り出しやすくなりテスタビリティが上がる

使い方

インストール

Gemfile
gem 'csb'
$ bundle install

基本

基本的には以下のようにコントローラでレコードをロードして、ビューにcsvの定義を書くといった流れになります。
これだけで自動的にストリーミングダウンロードとなります。

ダウンロード元のビュー

index.html.haml
= link_to 'CSVダウンロード', posts_path(format: :csv)

CSV出力用コントローラ

posts_controller.rb
def index
  @posts = Post.preload(:category)
end

CSV出力用コビュー

index.csv.csb
# csv.itemsにレコードを入れます(each可能であればActiveRecord::Relation以外のオブジェクトを入れることも出来ます)
csv.items = @posts

# 以下で各カラムを定義しています
csv.cols.add('投稿日') { |post| l(post.created_at.to_date) } # ブロックの場合引数にはレコードが渡ってきます
csv.cols.add('カテゴリー') { |post| post.category.name }
csv.cols.add('タイトル', :title) # 単純なメソッド呼び出しだけの場合はシンボルで書けます
csv.cols.add('本文', :content)

応用

大量データの場合

ただし大量データの場合、上記の書き方だと全レコードのロードが完了してからストリーミング開始となってしまうため、メモリエラーやタイムアウトエラーが発生しやすくなりストリーミングのメリットを活かせません。
大量データの場合は以下のようにcsv.items = @posts.find_eachと書いて、csv.itemsEnumeratorを渡すことでストリーミングのメリットを最大限に活かすことが可能となります。

index.csv.csb
csv.items = @posts.find_each
csv.cols.add('投稿日') { |post| l(post.created_at.to_date) }
csv.cols.add('カテゴリー') { |post| post.category.name }
csv.cols.add('タイトル', :title)
csv.cols.add('本文', :content)

大量データかつDecoratorと組み合わせる場合

大量データかつ、CSV出力用にデータを加工したいけど繰り返し同じ処理を書きたくないといったケースでは、専用のDecoratorクラスを用意することで以下のように書けます。(draperの例)

index.csv.csb
csv.items = @posts.find_each.lazy.map(&:decorate)
csv.cols.add('投稿日') { |post| l(post.created_at.to_date) }
csv.cols.add('カテゴリー') { |post| post.category.name }
csv.cols.add('タイトル', :decorated_title)
csv.cols.add('本文', :content)

特殊な出力

また外部システムとのcsv連携で時々発生する、以下のようなケースにも対応しています。

  • 固定文字列を出力したい
  • 中身は空でいいけど列は必須
  • 同名のカラムが複数必要
index.csv.csb
csv.items = @posts
csv.cols.add('固定文字列', 'Dummy') # 第二引数をシンボルではなく文字列にするとそのまま文字列として出力されます。
csv.cols.add('空文字列') # 第二引数を省略すると空文字となります。`csv.cols.add('空文字列', '')` と書くのと同じ。
csv.cols.add('複数カラム', :col1) # 同名カラムを複数定義することも可能です。(定義順に出力されます)
csv.cols.add('複数カラム', :col2)

Excelで文字化けせずに開きたい

以下のように出力文字コードをBOM付きUTF-8に設定することで、最近のExcelであればWindowsでもMacでも文字化けせずに開けるようになります。

config/initializers/csb.rb
Csb.configure do |config|
  config.utf8_bom = true # default: false
end

ダウンロードするcsvファイル名を設定したい

古いブラウザの考慮が不要で、必ずリンク経由でのダウンロードであればaタグのdownload属性を利用するのがお手軽ですが(参考リンク)、view側でも以下のように指定可能となっています。(Rails6以降であれば日本語も問題ないはずです)

index.csv.csb
csv.items = @posts
csv.filename = "posts_#{Time.current.to_i}.csv"

# ...

テスト

複雑な条件によるcsv出力の場合は当然テストも必要となりますが、system testで条件別に検証というのは面倒ですよね。
そんな場合はcsv出力の定義だけをモデル等に書くことで、テスタビリティを上げることが出来ます。

post.rb
def self.csb_cols
  Csb::Cols.new do |cols|
    cols.add('タイトル', :title)
    cols.add('本文', :content)
    cols.add('画像') { |post| post.image&.url }
  end
end
index.csv.csb
csv.items = @posts
csv.cols = Post.csb_cols
post_spec.rb
# requireすることでcol_pairsメソッドが追加で定義されます
require 'csb/testing'

context '画像が添付されている場合' do
  let(:image) { build(:image, url: 'https://example.test/example.jpg') }
  let(:post) { build(:post, title: 'Testing', content: 'hogehoge', image: image) }

  it '画像カラムにURLが出力されること' do
    expect(Post.csb_cols.col_pairs(post)).to eq [
      ['タイトル', 'Testing'],
      ['本文', 'hogehoge'],
      ['画像', 'https://example.test/example.jpg'],
    ]
  end
end

context '画像が添付されていない場合' do
  let(:post) { build(:post, title: 'Testing', content: 'hogehoge', image: nil) }

  it '画像カラムが空となること' do
    expect(Post.csb_cols.col_pairs(post)).to eq [
      ['タイトル', 'Testing'],
      ['本文', 'hogehoge'],
      ['画像', ''],
    ]
  end
end

※今後のバージョンアップでcsv全体での検証も書きやすくする予定です。

その他

オプション例

config/initializers/csb.rb
Csb.configure do |config|
  # デフォルトはfalseとなっているので注意してください。
  config.utf8_bom = true # default: false

  # 基本はデフォルトのままtrueでいいかと思いますが、大量データ配信が一切不要の場合はfalseとするほうがviewで発生するエラーに気付きやすいです。
  config.streaming = false # default: true

  # 何も設定しない場合、Bugsnag等のエラー検知ツールを入れていたとしてもストリーミング配信中のエラーは通知されないので注意が必要です。
  config.after_streaming_error = ->(error) do # default: nil
    Rails.logger.error(error)
    Bugsnag.notify(error)
  end
end

gemの名前の由来

csv builderの略です。
gemの名前とviewの拡張子を揃えようとすると短い名前の方が嬉しいので、語呂が良くて(日本語での)発音も似ているcsbとなりました。

リポジトリ

https://github.com/aki77/csb
良かったら使ってみてください。

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

rails チュートリアル

1.3.4からスタート

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

rails newする際のオプションとrails newした後に行う設定

個人的に「rails newをする際によく使うな」と感じるオプションと、新規プロジェクトを作成したときによく行う設定を備忘録を兼ねてまとめます。

rails newをする際のオプション

まずはrails newをする際によく付けるオプションについてです。
一つ一つのオプションについては後述します。

rails _6.0.3.2_ new appname --database=mysql --skip-test

railsのバージョン指定

rails _6.0.3.2_ new

こうするとrailsのバージョンを指定することができます。
_6.0.3.2_の部分はその都度値を変えてください。

使用するデータベースの指定

--database=mysql

このようにDBを指定しないと、デフォルトのDBはsqliteというものになります。
今回はmysqlを使用する設定です。

Minitestを生成しない

--skip-test

デフォルトでプロジェクトを生成すると、Minitestというものが作られます。
私自身、テストにはRSpecを使用することが多いため、上記のようにMinitestを生成しないようにしています。

rails newした後に行う設定

ここからは実際にrails newをした後の設定となります。

rails gコマンド使用時に、不要ファイルを生成しないように設定

config/application.rb
module appname
  class Application < Rails::Application
    # 以下を追加
    config.generators do |g|
      g.stylesheets false
      g.javascripts false
      g.helper false
      g.test_framework false
    end
    config.time_zone = "Tokyo"
    config.i18n.default_locale = :ja
  end
end

今回主に追加したのは下記の部分です。

config.generators do |g|
  g.stylesheets false
  g.javascripts false
  g.helper false
  g.test_framework false
end

rails gコマンドでコントローラを作成すると、ファイルが自動的に生成されてしまいますが、不要なもの(coffeeやcss)を生成しないようにするのがこの記述です。
rails gコマンドは開発の過程で頻繁に使用するものなので、この設定を最初に済ませておくと非常に楽になります。

config.time_zone = "Tokyo"
config.i18n.default_locale = :ja

また、こちらでデフォルトの言語を日本語にし、タイムゾーンのデフォルトを東京に設定します。

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