20200714のRailsに関する記事は18件です。

新・Railsポートフォリオ作成の備忘録②

5.UsersのProfileを作成する

以下記事を参照に、Usersのprofile作成をする。
https://qiita.com/ryokky59/items/71b5a853989721b89c6e

<注意点>
※ app/controllers/user/apprication_controller.rb
のファイルをいじるよう記載があるが、当方の場合、app/controllers/apprication_controller.rbが生成されたので、こちらをいじった。

※resources :users, only: [:show] と get 'users/show'の二つが作成されていたので、
resources :users, only: [:show] だけにした。

<生じたエラー>
上記記事を参照してshowページまで無事にたどり着くか試したかったが上手くいかなかった。

++++++++++++エラー内容++++++++++++++
このサイトにアクセスできません
localhost で接続が拒否されました。
++++++++++++++++++++++++++++++++++

この時、$ rails routes コマンドでルートが正しく設定されているか確認するよう教わった。
https://pikawaka.com/rails/rake_routes

<解決策>
以下のようにコードを書き換えるように教わった。

Rails.application.routes.draw do
  devise_for :sellers, controllers: {
  sessions:      'sellers/sessions',
  passwords:     'sellers/passwords',
  registrations: 'sellers/registrations'
}
  devise_for :users, controllers: {
    sessions:      'users/sessions',
    passwords:     'users/passwords',
    registrations: 'users/registrations'
  }
  resources :users, only: [:show] ← ここ
end

記事を参考にした際、resources :users, only: [:show]
を一番上に置いた。
ルーティングは一番上にあるコードが優先的になる為、
例えば、/users/sessionsも/users/passwordsも/users/:idのルーティングとして読み込まれ、引数の1のようにsesssionsとpasswordsも引数の一つとして扱われてしまっていた。

<その他修正点>

Vessel_Trader_Queen/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base 

   protect_from_forgery with: :exception 

   # deviseコントローラーにストロングパラメータを追加する 
   # before_action:configure_permitted_parameters, if: :devise_controller? 

   protected 
   def configure_permitted_parameters 
     # サインアップ時にnameのストロングパラメータを追加 
     devise_parameter_sanitizer.permit(:show, keys: [:name]) 
     # アカウント編集の時にnameとprofileのストロングパラメータを追加 
     devise_parameter_sanitizer.permit(:account_update, keys: [:name, :introduction]) 
   end 

   # ログイン後、マイページに移動する
   def after_sign_in_path_for(resource)
     user_path(resource)
   end 
 end 

Vessel_Trader_Queen/app/controllers/users_controller.rb

 class UsersController < ApplicationController 

   # ログイン済ユーザーのみにアクセスを許可する 
   before_action :authenticate_user!, only: [:show] 

   def show 
     if params[:id]
       @user = User.find(params[:id]) #追記 
     end 
   end 
 end 

Vessel_Trader_Queen/app/models/user.rb

class User < ApplicationRecord 

   # Include default devise modules. Others available are: 
   # :confirmable, :lockable, :timeoutable and :omniauthable 
   devise :database_authenticatable, :registerable, 
          :recoverable, :rememberable, :trackable, :validatable 

          before_save { self.email = email.downcase } 

         #  validates :name,  presence: true, length: { maximum: 50 }
          VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i 
          validates :email, presence: true, length: { maximum: 255 }, 
                            format: { with: VALID_EMAIL_REGEX } 
         #  validates :type, presence: true, length: { maximum: 50 }
         #  validates :industry, length: { maximum: 50 }
         #  validates :introduction,  length: { maximum: 1000 }

end

Vessel_Trader_Queen/app/views/users/show.html.erb

<h1>Users#show</h1>
<p>名前 : <%= @user.name %></p>
<p>メールアドレス : <%= @user.email %></p>
<p>プロフィール : <%= @user.introduction %></p>
<%= link_to "ログアウト", destroy_user_session_path, method: :delete %>

※正しこの時点では「名前とプロフィールが保存できなかった」

6.Sourcetreeの導入

Sourcetreeが便利だということでSourcetreeをインストールした。

7.git pull の使用

この時、先生がほとんどのコードを書き換えたので、git pull を使用した。
参考になったのは、以下のページである。

https://qiita.com/wann/items/688bc17460a457104d7d

git fetch + git merge = git pull

git cloneは、チーム開発の初期段階、特に自身のローカルにアプリのコードがない場合に使用する、というイメージ。

8.名前とプロフィールが保存できない問題の解決

5.のコードを更に以下のように修正した。
また、他の項目も付け足した。

Vessel_Trader_Queen/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base 

   protect_from_forgery with: :exception 

  # deviseコントローラーを実行する前に、configure_permitted_parametersメソッドを実行する
  before_action :configure_permitted_parameters, if: :devise_controller?

  # sign_upアクション時にnameとintroductionカラムの保存を許可する
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :introduction, :industry, :user_type ])
  end

  # ログイン後、マイページに移動する
  def after_sign_in_path_for(resource)
    user_path(resource)
  end
 end 

Vessel_Trader_Queen/app/controllers/users_controller.rb

 class UsersController < ApplicationController 

   # ログイン済ユーザーのみにアクセスを許可する 
   before_action :authenticate_user!, only: [:show] 

   def show 
     if params[:id]
       @user = User.find(params[:id]) #追記 
     end 
   end 
 end 

Vessel_Trader_Queen/app/models/user.rb

class User < ApplicationRecord 

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

         before_save { self.email = email.downcase }

        #  validates :name,  presence: true, length: { maximum: 50 }
         VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
         validates :email, presence: true, length: { maximum: 255 },
                           format: { with: VALID_EMAIL_REGEX }
end

Vessel_Trader_Queen/app/views/users/show.html.erb

<h1>Users#show</h1>
<p>名前 : <%= @user.name %></p>
<p>メールアドレス : <%= @user.email %></p>
<p>自己紹介 : <%= @user.introduction %></p>
<p>業種 : <%= @user.industry %></p>
<p>業務形態 : <%= @user.user_type %></p>
<%= link_to "ログアウト", destroy_user_session_path, method: :delete %>

config/routes.rb

Rails.application.routes.draw do
  devise_for :sellers, controllers: {
    sessions:      'sellers/sessions',
    passwords:     'sellers/passwords',
    registrations: 'sellers/registrations'
  }

  devise_for :users, controllers: {
    sessions:      'users/sessions',
    passwords:     'users/passwords',
    registrations: 'users/registrations'
  }
  resources :users, only: [:show]
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】多階層カテゴリーから商品一覧を表示させるプロセスとは

前提条件

以下のことが終わっていることとする。終わっていないと機能が正しくなっているか確認が難しい。
・ancestryを用いてカテゴリーテーブルを作成している。
・カテゴリー:商品 = 1:多の関係になっている。
・カテゴリーの階層が3段階になっておりそれぞれの命名が「親」、「子」、「孫」になっている
・商品モデルのcategory_idには、最下層のカテゴリーidが登録されている。
・gem kaminariがインストール済
・カテゴリーコントローラーファイルの作成及び、カテゴリーの取得ができるコードが書かれている。

不安な人はここをクリック
以下のようなコードが書かれていれば、問題ない。
_Rails_多階層カテゴリーから商品を検索・一覧表示する機能_-_Qiita.png
_Rails_多階層カテゴリーから商品を検索・一覧表示する機能_-_Qiita.png

カテゴリー別に商品一覧を表示させる

カテゴリーの詳細が見たいわけなのでshowアクションを用います。

showアクション定義

app/controllers/categories_controller.rb

before_action :set_category, only: :show

def show
  @items = @category.set_items
  @items = @items.where(buyer_id: nil).order("created_at DESC").page(params[:page]).per(9)
end

private
def set_category
  @category = Category.find(params[:id])
end

モデルメソッド定義

app/models/category.rb

has_many :items
has_ancestry

def set_items
  # 親カテゴリーだった場合
  if self.root?
    start_id = self.indirects.first.id
    end_id = self.indirects.last.id
    items = Item.where(category_id: start_id..end_id)
    return items

    # 子カテゴリーだった場合
  elsif self.has_children?
    start_id = self.children.first.id
    end_id = self.children.last.id
    items = Item.where(category_id: start_id..end_id)
    return items

    # 孫カテゴリーだった場合
  else
    return self.items
  end
end

@items = @category.items と記述するだけでは、
商品モデルのcategory_idには最下層のidが付与されているので@categoryが孫であった場合のみ、情報が取得されることになってしまう。
なので予め、上記のようにカテゴリーが親なのか子なのか孫なのかといったような条件分岐をするとうまくいくはず

ビュー

app/views/categories/show.html.haml

.items-container
  .items-index
    .title
      = "#{@category.name}の商品一覧"
      .title__border
    - if @items
      %ul.lists
        = render "items/item", items: @items
    = paginate @items

このままでは他のカテゴリーへのリンクがまだ設定されていないが、
それはまた、書いていきます

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

【before_action】

before_actionとは

before_actionはコントローラーで定義された処理を実行する前に共通の処理を行うことができるメソッドのこと

今回はコントローラー内のメソッドの実行内容が重複している場合のbefore_actionの使用例を書いていきます。

set_action1.png

上の画像のようにeditアクションとshowアクションの実行内容が同じである場合は共通の処理としてまとめてしまった方が可読性も上がり、変更の際も便利なので処理をまとめる。

set_action2.png

まず該当のアクションを削除。

set_action3.png

その後先ほど削除した共通していた処理内容をprivateメソッドの部分にset_actionとして定義する。

set_action5.png

最後にコントローラーの上部にbefore_actionを記述する。
今回の場合はset_actionを適用させたいのはeditとshowアクションのみなのでオプションとしてonlyを記述することで該当のアクションにのみset_actionを実行させれば良い。

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

Ruby on RailsのポートフォリオにDockerを組み込む!

現在の状態

みなさんこんにちわ今回Qiita初投稿を行います!
間違っているところがあると思いますがその時は優しく教えてください(笑)
今回、AwsとDockerを用いて既に作成済のRailsのポートフォリオをデプロイしています。
現在の状態は、railsとAws用いてポートフォリオを作成したのですが、環境構築が大変だと思い、環境周りをコードで管理しやすいDockerを用いてポートフォリオを際デプロイしたいと思います。

構成図

デプロイの際の構成図は以下の通りです。
スクリーンショット 2020-07-14 21.27.20.png
めちゃくちゃ簡単に説明するとクライアントから通信リクエストが来るとNginxに行き動的な処理が必要な際はpumaに通信を行いその際にpumaとmysqlも通信する流れとなっています。
今回はこの構成をDockerを用いて作成していきたいと思います。

必要な物

今回デプロイする際はAwsのEC2とRDSをを使用します。
Dockerで環境構築を行いますが、データの永続化の観点から今回データベースはDockerではなくRDSのMysqlをしようします。
以下作業に必要な準備と作業です。

・AwsでEC2、RDSを用いてデプロイする際に必要な準備を行う(ネットワーク構成など...)
・local環境とEc2にDokcerをインストール
・local環境とEc2にDocker-composeをインストール

参考にした記事

EC2上でRailsアプリケーションにDockerを導入する(Rails、Nginx、RDS)
Rails On DockerでのAWSデプロイができたので,中身を整理します。

作業

まずはじめにDockerファイルを作成します。
ここではDockerfileを用いてRuby周りの環境を構築するコードを書きます。

FROM ruby:2.5.7

RUN apt-get update -qq && \
    apt-get install -y build-essential \
                       libpq-dev \
                       nodejs \
                       vim

RUN mkdir /アプリの名前

WORKDIR /アプリの名前

ADD Gemfile /アプリの名前/Gemfile
ADD Gemfile.lock /アプリの名前/Gemfile.lock

RUN gem install bundler
RUN bundle install

ADD . /アプリの名前

RUN mkdir -p tmp/sockets
RUN mkdir -p tmp/pids

次にDocker-composeを作成します。
ここではDockerコンテナの管理やマントする箇所の指定を行っていきます!

Docker-compose.yml
version: '3'
services:
  app:
    build: .
    command: bundle exec puma -C config/puma.rb -e production
    volumes:
      - .:/アプリの名前:cached
      - public-data:/アプリの名前/public
      - tmp-data:/アプリの名前/tmp
      - log-data:/アプリの名前/log

  web:
    build:
      context: containers/nginx
    volumes:
      - public-data:/アプリの名前/public
      - tmp-data:/アプリの名前/tmp
    ports:
      - 80:80

volumes:
  public-data:
  tmp-data:
  log-data:

次に以下のファイルを作成します。
ここではNginxコンテナの設定を行っていきます。

FROM nginx:1.15.8

RUN rm -f /etc/nginx/conf.d/*

ADD nginx.conf /etc/nginx/conf.d/アプリの名前.conf

CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
containers/nginx/nginx.conf
upstream FashionInformation_app {
  server unix:///アプリの名前/tmp/sockets/puma.sock;
}

server {
  listen 8000;
  server_name ドメイン名;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;

  root /アプリの名前/public;

  client_max_body_size 100m;
  error_page 404             /404.html;
  error_page 505 502 503 504 /500.html;
  try_files  $uri/index.html $uri @アプリの名前;
  keepalive_timeout 5;

  location @アプリの名前 {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://アプリの名前;
  }
}

Railsアプリケーションをデプロイする

ここまできたらEC2にgitをクローンします!

ec2-user@ip-xxx-xx-xx-xxx ~]$ git clone GitHubのリポジトリのURL

イメージのビルド

ec2-user@ip-xxx-xx-xx-xxx]$ cd myapp
[ec2-user@ip-xxx-xx-xx-xxx myapp]$ docker-compose build

サーバー起動前の準備

[ec2-user@ip-xxx-xx-xx-xxx myapp]$ docker-compose run app rails assets:precompile RAILS_ENV=production

サーバー起動前の準備

[ec2-user@ip-xxx-xx-xx-xxx myapp]$ docker-compose up -d

データベースの作成、マイグレーションファイルの読み込み

[ec2-user@ip-xxx-xx-xx-xxx myapp]$ docker-compose exec app rails db:create db:migrate RAILS_ENV=production

パブリックIPにアクセスして、正しく表示されれば成功です!

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

Docker+Rails環境でActive Storageを導入する

Active Storageとは?

Rails5.2系から利用可能な、ファイルアップローダーです。
develop(開発)環境ではローカルに画像を保存、product(本番)環境ではAmazon S3やGCS、Microsoft Azure Storageに画像を保存したりと、ファイルのアップロード先を簡単に切り替えることができます。

ファイルアップローダー用のGemでは、carrierwave などが有名ですが、新規に画像用のカラムを追加しなければいけなかったりと、なかなか大変です。
対するActiveStorageは、画像追加したいテーブルに対してカラム追加をする必要がありません。carrierwaveよりお手軽に導入できるので、その手順を紹介していきたいと思います。

導入手順

※既に投稿機能は実装済みであることが前提です。(Productモデル、Articleモデルなど)
今回は、dev環境でAmazon S3にアップロードする手順を紹介していきます。

Active Storageのインストール

$ docker compose run web rails active_storage:install
$ docker compose run web rails db:migrate

すると、active_storage_blobsactive_storage_attachmentsという名前の2つのテーブルが作成されます。

active_storage_blobsは実際にアップロードしたファイルが保存されるテーブルで、active_storage_attachmentsは中間テーブルになります。

【Rails】Active Storageを使って画像をアップしよう! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト様より引用

実際にActive Storageを扱っていく上で、上記の2つのテーブルには一切触れません。

単一ファイルのみアップロードする場合

product
class Product < ApplicationRecord
  has_one_attached :image #カラム名を記述
end

上記、カラム名と書いていますが好きな名前でいいです。(僕はimageとしています。)
カラム名と書いていますが、実際にProductモデルにimageカラムは追加する必要がありません。

products_controller.rb
  def new
    @product = Product.new
  end

  def create
    @product = Product.new (product_params)
    @product.producer_id = current_producer.id
    if @product.save
      redirect_to product_path(@product), notice: "投稿に成功しました"
    else
      render :new
    end
  end

 def product_params
    params.require(:product).permit(:name, :description, :price, :content, :image)
  end

ストロングパラメーターに、 :image を追加します。

new.html.erb
<h2>商品の出品</h2>
<%= form_for @product do |f| %>
  <label>商品名</label>
  <p><%= f.text_field :name %></p>

  <label>出品時のタイトル</label>
  <p><%= f.text_field :content %></p>

  <label>商品の値段</label>
  <p><%= f.number_field :price %></p>

  <label>画像</label>
  <p><%= f.file_field :image %></p> #画像投稿部分

  <label>商品内容</label>
  <p><%= f.text_area :description %></p>

    <%= f.submit %>

<% end %>

画像を表示する際は、 <%= image_tag @product.image %>とすれば表示できます。

複数ファイルをアップロードする場合

product.rb
class Product < ApplicationRecord
  has_many_attached :images #複数形
end
products_controller.rb
  def new
    @product = Product.new
  end

  def create
    @product = Product.new (product_params)
    @product.producer_id = current_producer.id
    if @product.save
      redirect_to product_path(@product), notice: "投稿に成功しました"
    else
      render :new
    end
  end

 def product_params
    params.require(:product).permit(:name, :description, :price, :content, images: []) #images: []という形で、配列にする
  end
new.html.erb
<h2>商品の出品</h2>
<%= form_for @product do |f| %>
  <label>商品名</label>
  <p><%= f.text_field :name %></p>

  <label>出品時のタイトル</label>
  <p><%= f.text_field :content %></p>

  <label>商品の値段</label>
  <p><%= f.number_field :price %></p>

  <label>画像</label>
  <p><%= f.file_field :images, multiple: true %></p> #画像投稿部分

  <label>商品内容</label>
  <p><%= f.text_area :description %></p>

    <%= f.submit %>

<% end %>
show.html.erb
<% if @product.images.attached? %>
  <% @product.images.each do |image| %>
    <%= image_tag image %> <br>
  <% end %>
<% end %>

<% if @product.images.attached? %> で、@productが画像を持っているかどうかを判断しています。
@productが持っているimages(画像)を、each文で回して images[ ] の中身を全て表示しています。

ファイルの保存先の指定

config/environments/development.rb を確認します。

  config.active_storage.service = :local

という部分がありますが、これはデフォルトでローカルに保存するように設定されています。
S3に保存したい場合は、

  config.active_storage.service = :amazon

に変更します。

次に config/storage.yml を設定していきます。
amazon、GCS,AzureStorageがコメントアウトされてますが、amazonの部分のコメントをすべて外しましょう。

config/storage.yml
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: "ap-northeast-1" # 東京リージョン
  bucket: "S3で自分が作ったバケット名"

Gemfileを追加します。

gem "aws-sdk-s3", require: false

Gemfileを追加したので、docker-compose build します。(この部分は人によります。)

$ docker-compose build

認証情報の記述

credentials.yml.encに、アクセスキーとシークレットアクセスキーを記述していきます。
Docker無しの環境であれば、

$ EDITOR=vim rails credentials:edit

で行けるのですが、Docker環境の場合上記のコマンドでは上手くいきませんでした。
Rails on Dockerでcredentialsをeditしたい
の記事がとても分かりやすく、Docker+rails環境ででcredential編集ができるようになります。

 aws:
   access_key_id: アクセスキー
   secret_access_key: シークレットアクセスキー

上記3行のコメントアウトはすべて外しましょう。

バケットとIAMユーザーの作成

めちゃくちゃ長くなるので割愛しますが、大まかにいうと、
1. IAMユーザー作成(AmazonS3FullAccessのポリシーを付与)
2. IAMユーザー作成時にアクセスキー、シークレットアクセスキーが記述されたcsvファイルをダウンロード(1回しかダウンロードできないので、管理は厳重にしておく)
2. S3のバケットを作成

上手くいけばS3アップロード成功となります。

参考にさせていただいた記事

【Rails 5.2】 Active Storageの使い方 - Qiita

Rails on Dockerでcredentialsをeditしたい

https://railsguides.jp/active_storage_overview.html

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

Deviseで作成したUserモデル等に対して、プロフィール画像投稿機能を追加する

Deviseで作成したUserモデル等に対して、プロフィール画像投稿機能を追加する

前提:DeviseでUserモデルを作成していること

手順

Railsで画像をアップロードする方法を現役エンジニアが解説【初心者向け】 | TechAcademyマガジンより引用。

CarrierWaveの使い方
CarrierWaveは、以下の3ステップで使用できるようになります。

  1. gemをインストールする
  2. アップローダーを作成する
  3. モデルにカラムを追加し、マウントする 上記の3つの準備をした上で、画像を保存すると画像がpublicフォルダに保存されます。

このようにして進めていきます。

1. Gemのインストール

Gemfile.rb
gem 'carrierwave'
$ bundle install

2. アップローダーの作成

$ rails g uploader Image

3. モデルにカラムを追加し、マウントする

deviseで作成したUserモデルに対して、カラムを追加していきます。
カラム追加のマイグレーションスクリプトは、下記の形式になります。

$ rails g migration Addカラム名(先頭大文字)Toテーブル名

今回は、imageカラムをusersテーブルに追加したいので、

$ rails g migration AddImageToUsers image:string

deviseはデフォルトの状態だとemailとpasswordしか受け取らない設定になっているので、カラム追加した際は、新しくパラメーターを設定しなければいけません。
今回はimageカラムを追加したので、imageカラムを許可する設定をapplication_controller.rbに記述してあげます。

application_controller.rb
def configure_permitted_parameters
  devise_parameter_sanitizer.permit(:sign_up, keys: [:image]) 
end

アップロードを実装したいモデル、今回はuserモデルに下記のように記述します。
(投稿にも画像を添付したい場合は、Postモデル等にも同じように記述します。)

user.rb
mount_uploader :image, ImageUploader #追加

この記述によって、画像アップローダーを実装することが可能です。

新規登録時にプロフィール画像を設定

新規登録時にプロフィール画像を設定し、かつユーザー編集画面でもプロフィール画像を編集できるようにしたいと思います

app/views/devise/registrations/new.html.erbを編集します。

new.html.erb
=================================ここから
  <div class="field">
  <%= f.label :image %><br/>
  <%= f.file_field :image %>
</div>
=================================ここまで追加
・・・・・・・・・・・・・・・
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

ユーザー編集画面でプロフィール画像を設定

users_controller.rb
class UsersController < ApplicationController
  def index
  end

  def edit
    @user = current_user
  end

  def update
    @user = current_user
    if @user.update(user_params)
      redirect_to user_path, notice: "ユーザーを更新した"
    else
      render :edit
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :image)
  end

end

edit.html.erb
<h2>編集</h2>

<%= form_for @user do |f| %>

<label>メールアドレス</label>
<%= f.text_field :email %>

<label>プロフィール画像</label>
<%= f.file_field :image %>


<%= f.submit %>

<% end %>

テンプレート側で画像を表示したい場合は、Userモデルから情報を取ってきて、
<p><%= image_tag user.image.url %></p>とすれば表示可能となります。

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

[Rails]payjp(API)を利用する仕組みと方法をかなり丁寧に書いてみる(クレカ登録編)

はじめに

スクールの課題でフリマアプリを作る中で、payjpを利用したクレカ登録・決済機能の実装がありました。
検索すると以前の卒業生が多数記事を書いてくれてるのでコードは見つかるのですが、何をやっているの?という所は結局公式のリファレンス読むのが一番だよねとなったので理解したことについて記載しておくものです。

この記事では
payjpを利用するってそもそもどういうこと?(APIの説明)
payjpの呼び出し方
クレカ登録の実装
登録したクレカによる決済実装

上記について記載していきます。

※現在payjpはv2バージョンを利用することが推奨されております。よりセキュアな実装として登録フォーム自体もpayjp側で用意してくれているのがv2なのですが、今回の課題はフォームの実装もこちら側のタスクとして含まれていたため、旧型のv1で実装を進めています。
とはいえ呼び出し方や使えるメソッドが異なるだけで、どちらも公式を読みながらやればあまり差がなく実装できるかと思います。

環境

ruby 2.6.5
rails 6.0.3

payjpを利用するってそもそもどういうこと?

コードを書いていくにしても、前段としてここの理解がまず大事だと思っています(あまりここに触れた記事はなかった)

payjpは、APIの1種です。
APIっていうのはググれば色々説明が出てくるんですが、「外部向けにソフトウェアの機能を一部提供してあげるよ」って感じです。

用意してくれてる側の指示に従いうまくAPIを呼び出すことで、自分のアプリケーションの中でそのソフトウェアの機能を一部使えることができる様になる、という仕組みですね。

具体例を言うと、例えばGogole Mapのサービスがあります。
あれもgoogleが用意してくれたAPIを利用し、指示にしたがってコードを記載することで、自分のアプリケーション上にgoogle mapを表示することが可能になるという仕組みです。

payjpもクレカに関するAPIであり、利用することでクレジットカードの登録や決済が可能になります。

APIを利用する時に考え方として個人的に大事だと思っているのが、相手に用意してもらっているという流れをちゃんと把握するということです。

上記を認識した上で実装の流れを考えると、
用意してもらったやり方で、APIを呼び出す準備をする
用意してもらったやり方で、向こうのアプリケーションとやりとりをし、処理を行う

といった感じになります。このイメージがあると、以降の作業が具体的に何をしているのか分かりやすくなると思うので重要です。

それが故、用意してくれてる側の公式ドキュメントでやり方を理解する、と言うのが一番適切なアプローチになります。公式の説明書を読む様なものなので。

payjpの呼び出し方

前提として、payjpに登録しテスト公開鍵とテスト秘密鍵を取得しましょう。
APIの認証のために必要なキーです。payjp側はこれらを持って、僕らが「利用を許されたユーザー」であることを判断しています。

登録が終わったらv1の公式リファレンスを読みましょう。
アクセスして早々、埋め込むべきスクリプトがちゃんと記載されています。
また、少し下に読み進めると、公開鍵による認証方法も書いています。
実際の記載については次項で見ていきます。

クレカ登録の実装

上記の呼び出し方も踏まえ、先に実際のコードを載せます。
その上で流れを解説していきます。
※各ファイル、説明に不要な部分の記載は適宜削除しています。

application.html.haml
!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %script{src: "https://js.pay.jp/v1/", type: "text/javascript"}
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application'
new.html.haml
    .creditCreate
      .creditCreate__title
        %h1 クレジットカード情報入力
      .creditForm
        = form_with model: @credit, method: :post, id: "cardCreateForm" do |f|
          .creditForm__numberfield
            %label(for="cardnumber-input") カード番号
            %span.creditPoint 必須
            %br
            = f.text_field :card_number, type: "text", class: 'cardnumber-input', id:'card-number', placeholder: "半角数字のみ", maxlength: 16
            .creditslist
              = image_tag "jcb.gif", class:"creditsIcon"
              = image_tag "visa.gif", class:"creditsIcon"
              = image_tag "master.gif", class:"creditsIcon"
              = image_tag "amex.gif", class:"creditsIcon"
          .creditForm__datefield
            %label 有効期限
            %span.creditPoint 必須
            %br
            = f.select :exp_month, [["01",1],["02",2],["03",3],["04",4],["05",5],["06",6],["07",7],["08",8],["09",9],["10",10],["11",11],["12",12]],{} , class: 'dateSelect', name: 'exp_month'= f.select :exp_year, [["20",2020],["21",2021],["22",2022],["23",2023],["24",2024],["25",2025],["26",2026],["27",2027],["28",2028],["29",2029]],{} , class: 'dateSelect', name: 'exp_year'.creditForm__securityfield
            %label セキュリティコード
            %span.creditPoint 必須
            %br
            = f.text_field :cvc, type: 'text', class: 'securityInput', id: 'cvc', placeholder: 'カード背面4桁もしくは3桁の番号', maxlength: "4"
          #card_token.creditForm__submitfield
            = f.submit '追加する', class: 'creditsSubmit', id: 'token_submit'

payjp.js
$(function() {
  $('#cardCreateForm').on('submit', function(e) {
    e.preventDefault()
    Payjp.setPublicKey(['PAYJP_PUBLIC_KEY']);
    var card = {
      number: document.getElementById("card-number").value,
      exp_month: document.getElementById("credit_exp_month").value,
      exp_year: document.getElementById("credit_exp_year").value,
      cvc: document.getElementById("cvc").value
    };
    if (card.number == "" || card.cvc == "") {
      alert("入力もれがあります");
    } else {
      Payjp.createToken(card, function(status, response) {
        if (status === 200 ) {
          $("#card_number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name");
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          );
          $('#cardCreateForm').get(0).submit();
          alert("登録に成功しました");
        } else {
          alert("カード情報が正しくありません");
        }
      });
    }
  });
});
credits_controller.rb
require 'payjp'

def create
    Payjp.api_key =  ENV['PAYJP_SECRET_KEY']
    if params['payjp-token'].blank?
      render :new
    else
      customer = Payjp::Customer.create(
        email: current_user.email,
        card: params['payjp-token'],
        metadata: {user_id: current_user.id}
      )
      @credit = Credit.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      if @credit.save
        redirect_to user_path(current_user.id)
      else
        render :new
      end
    end
  end

以上がクレカ登録に関するコードです。
処理の流れは以下の通りです。この流れに沿って説明をしていきます。

①payjpのAPIを利用するセッティングをする
②jsファイルにて、フォーム送信時に入力内容とpajypを紐付け登録の準備をする
③コントローラーにアクションを飛ばし、payjp上で顧客データを生成・顧客データを紐づくidをテーブルに保存する

①payjpのAPIを利用するセッティングをする

application.html.hamlをみましょう

application.html.haml
!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %script{src: "https://js.pay.jp/v1/", type: "text/javascript"}
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application'

scriptの部分がpayjpを呼び出す記載です。
呼び出し方は公式リファレンスに書いてあります。ただ転記するだけです。公式をみていればただ転記するだけなのです。

②jsファイルにて、フォーム送信時に入力内容とpajypを紐付け登録の準備をする

jsファイルをみましょう。
フォーム送信のhamlファイルと対応した記載になってるので、並べてみながらだと理解がしやすいと思います。

payjp.js
$(function() {
  $('#cardCreateForm').on('submit', function(e) {
    e.preventDefault()
    Payjp.setPublicKey(['PAYJP_PUBLIC_KEY']);
    var card = {
      number: document.getElementById("card-number").value,
      exp_month: document.getElementById("credit_exp_month").value,
      exp_year: document.getElementById("credit_exp_year").value,
      cvc: document.getElementById("cvc").value
    };
    if (card.number == "" || card.cvc == "") {
      alert("入力もれがあります");
    } else {
      Payjp.createToken(card, function(status, response) {
        if (status === 200 ) {
          $("#card_number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name");
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          );
          $('#cardCreateForm').get(0).submit();
          alert("登録に成功しました");
        } else {
          alert("カード情報が正しくありません");
        }
      });
    }
  });
});

分割して解説していきます。

payjp.js
$(function() {
  $('#cardCreateForm').on('submit', function(e) {
    e.preventDefault()

jQueryで、フォームボタンをクリックした際の挙動ですと定義しています。

payjp.js
Payjp.setPublicKey(['PAYJP_PUBLIC_KEY']);

認証(payjpの利用登録ができていますよ!と言うこと)を示すため、登録し取得したテスト公開鍵をセットします。
これを持ってpayjp側が利用を許可してくれます。

setPublicKeyという書き方がいきなり出てきますが、これも公式リファレンスで指定された方法で記述してます。ただのコピペです。恐ることはありません。

payjp.js
    var card = {
      number: document.getElementById("card-number").value,
      exp_month: document.getElementById("credit_exp_month").value,
      exp_year: document.getElementById("credit_exp_year").value,
      cvc: document.getElementById("cvc").value
    };

JSの記載です。get element by id と言う書き方の通り、各フォーム欄のidを指定・特定することで、それぞれの入力内容を定義し、cardと言う変数に代入し定義しています。

payjp.js
    if (card.number == "" || card.cvc == "") {
      alert("入力もれがあります");

jsの記載です。定義した変数cardの、numberもしくはcvcが空だった時に受付をせずエラーを返す処理をしています。

payjp.js
    } else {
      Payjp.createToken(card, function(status, response) {
        if (status === 200 ) {
          $("#card_number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name");
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          );
          $('#cardCreateForm').get(0).submit();
          alert("登録に成功しました");
        } else {
          alert("カード情報が正しくありません");
        }
      });
    }
  });
});

カード情報を基にトークンを生成します。
これもpayjp APIの公式リファレンスを読んでください。

トークンを生成の部分を読むと、トークンはどのように生成するのかと言う文章での説明と、実際の記載方法が右側に記載されているはずです。

いきなりPayjp.create~と言う記載が出てきましたが、これはリファレンスに「この通り書いたらできるよ」って書いてくれてるだけなので、ちゃんと読めばその通りにするだけです。
ここでnumber等4つの値を渡す必要があると書かれいてがために、先ほどcard変数に4つの値を定義した訳です。順番的にはここで必要とされてるから定義している訳です。

if status === 200とは、カード情報が有効であり、正しくトークンが生成された場合にpayjp側が返してくれるステータス値になります。
なのでelseでエラーを返しているのは「正常に登録できなかった場合」を示しているわけです。

ここからさらに細かくみていきます。

payjp.js
          $("#card_number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name");

JS,jQueryの記載です。
カードが有効だった場合、セキュリティの観点からそれぞれのフォームに入力した値を取り除いています(idで指定したフォームのname属性をremoveする、と言う処理です)

payjp.js
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          );
          $('#cardCreateForm').get(0).submit();
          alert("登録に成功しました");

カードが有効な際にPayjpから帰ってきたデータ(response.id)を、フォームに返す処理をappendで行っています。
type = hiddenを指定することで、あくまでユーザーからは見えないものの、フォームにデータを送っている形です。

その上で、submitすることで、Payjpから返してもらったデータをこの後controllerに送りcreateアクションを行っていく、と言うことを実現しています。

コントローラーにうまくデータを飛ばすための記載ということですね。
これを持って次にcontrollerの処理をみていくことができます。

③コントローラーにアクションを飛ばし、payjp上で顧客データを生成・顧客データを紐づくidをテーブルに保存する

credits_controller.rb
require 'payjp'

def create
    Payjp.api_key =  ENV['PAYJP_SECRET_KEY']
    if params['payjp-token'].blank?
      render :new
    else
      customer = Payjp::Customer.create(
        email: current_user.email,
        card: params['payjp-token'],
        metadata: {user_id: current_user.id}
      )
      @credit = Credit.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      if @credit.save
        redirect_to user_path(current_user.id)
      else
        render :new
      end
    end
  end

ここまでうまく処理ができて、最後にcontrollerでデータ作成となります。
順を追ってみていきましょう。

credits_controller.rb
require 'payjp'

def create
    Payjp.api_key =  ENV['PAYJP_SECRET_KEY']
    if params['payjp-token'].blank?
      render :new

まずrequire 'payjp'でpayjpを使える様にします。
次にSECRET_KEYを指定し、こちらがpayjpの利用権を思ったユーザーであることを示します(環境変数を用いて記載しています)

その上で先ほど正常なレスポンスだった場合に送られてきたデータがparams['payjp-token']ですので、そちらがからだった場合は処理を行わずrenderする処理を記載しています。

credits_controller.rb
def create

    else
      customer = Payjp::Customer.create(
        email: current_user.email,
        card: params['payjp-token'],
        metadata: {user_id: current_user.id} #ここは任意
      )

ここで、Payjpの顧客データを作成し、変数customerに代入しています。
いきなりこの書き方が出てきました。
何度も言う様ですが公式リファレンスを見れば全てが買いてあります。

今回行いたいのは顧客データの作成なので、リファレンスの中の顧客データの欄をみます。
すると、Payjp::Customer.createという記載方法や、その引数が書かれているはずです。

引数の内容をみていけば何をいれるべきなのかは簡単にわかるはずです。
例えばcardと言う引数はトークンIDを指定と書かれているので、先ほど送る様に設定したトークンIDを設定すればいいことがわかります。

credits_controller.rb
def create

      @credit = Credit.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      if @credit.save
        redirect_to user_path(current_user.id)
      else
        render :new
      end
end

ここからはRails側のテーブル処理の話です。
先ほどのリファレンスを参照すれば、Payjpの顧客が持つレスポンスデータについても確認することができます。
先ほど作成したPayjpの顧客データは変数customerに代入していたため、customer.id等で指定することで、レスポンスデータをテーブルに保存することができるわけです。

(法律上クレカデータを自分のテーブルに保存することはできないため、そのデータ自体はpayjp側に保存してもらい、そのデータを呼び出すための紐付けとしてcustomer_idやcardのdataをテーブルに保存しておくわけです。

終わりに

結構な長文になってしまったので登録のみで終わらせますが、大事なのは流れを理解し、リファレンスを読むこと、これに尽きると思います。

この登録したデータを基にカード決済を行う流れについても、リファレンスを読みながら決済にはどの様な記述が必要か?引数には何を指定するのか?を見ることでわりとすんなり実装できました。
(先ほどテーブルに紐付けたことからわかる通り、決済に必要なカードデータを呼び出して引数に渡してやれば良い、と言うのはそれほど難しくなく想像できるのではないでしょうか)

とはいえAPIに慣れていない状態だったのでQiitaの記事にも大分助けられつつの実装にはなりました。が、結論公式が最強、この言葉を実感を持って理解できる経験だったので記録しておきます。

*初学者ゆえ何かあればご指摘いただけると嬉しいです。

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

[Rails]EC2にデプロイ後、jQuery/JavaScriptが動かない

前提

Rails 6.0.3.1
ruby 2.6.3
unicorn-5.5.5
nginx/1.16.1

 EC2にデプロイしたら、jQueryがお仕事をサボっていた

デプロイ後に、ブラウザでトップページを確認すると jQueryが動いておらず、トップページのfade inがいつまでたってもなされない状態でした。

結論

app/views/layouts/_default.html.erb
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

#以下を追加
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

ローカルでは、

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

なしでも動いていましたが、どうやら本番環境では追加しないとうまく動かない場合があるみたいです。

ちなみに
httpsではなく、httpでも動くようですが、SSL化したアプリケーションでは大体のアプリケーションは、セキュリティの面でSSL化すると思うので、httpsでしてあげた方が良さそうです。

 > railsでjQuery/JavaScriptがうまく動かない時の対処法

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

【Rails】チーム開発している時の頻出DB系コマンド

【こんな人に読んで欲しい】

チーム開発やってるけど、DB系コマンドが全然わからない。なんかマイグレーション系のエラーが起きる。どうなってるの?

【この記事を立ち上げた経緯】

ブランチを切ってチーム開発をしている時、(特に初期段階)、各自が各々のブランチでDBを作ってマイグレーションしてしまうので、
マイグレーションファイルやデータベース(テーブル)の状態がバラバラ。
そんな時にブランチを移動してしまうとエラーが起きる原因となります。それをどうやって修正するか?の記事です。かなりよく使うコマンドだと思うので、私もまだまだ勉強中ですが、少しでもご参考になれば幸いです。

後半に、本番環境のDBコマンドも載せてあります。


【バージョンやら環境】

Rails 5.2.4.3
Ruby 2.5.1
macOS Catalina 10.15.4
AWS

【開発環境(ローカル)】

まずは、現在のマイグレーションファイルの状態から確認。
マイグレーションファイルがデータベース(テーブル)に適用されているかどうかを調べるコマンドです。

ターミナル
rails db:migrate:status 

例えばこんな感じ
DB1.png
Migration IDとMigration Nameは、db/migrateの中のファイルとリンクしています。

■Status
・「up」 ▶︎ 適用されている
・「down」 ▶︎ 適用されていない(削除や修正が可能)



ちなみに、
・「********** NO FILE **********」 と表示される場合があります。これは、マイグレーションファイル一回作ったことあるけど、いまはdb/migrate/ディレクトリの下に見当たらないよって場合に表示されるらしいです。


状態もわかったので、ロールバックします。

ターミナル
rails db:rollback STEP=X

ロールバック、とは「一度データベースからマイグレーションファイルを差し戻す」こと。
「X」には戻したいファイルの数を入れます。4つ戻したければ4。下のファイルからdownになっていきます。
ロールバックすることで、ファイルをupからdown(*****)の状態にします。

参考画像では6つファイルありますが、Xに4を入れましてすロールバックするとこんな感じ
DB3.png

さらに2つロールバックすると全部downになります。
DB2_down.png



全部 downになったので、再度マイグレーションしてテーブルを再作成していきます。

ターミナル
rails db:migrate

なれない時は都度rails db:migrate:statusで状態を確認してみるといいと思います!!



ちなみに、うまくいかない場合はrails db:migrate:resetという、リセットしてマイグレーションするコマンドもあります。

ターミナル (リセットとマイグレーションを同時に行う)
rails db:migrate:reset

どちらにしても、にローカルで保存していた画像やら登録したユーザーやらも消えてしまいますが・・・まあ、やむなしですね。

ちなみに、私の場合は、基本元のブランチで全てdownにしてから、作業するブランチに行ってrails db:migrateをしていました。




【参考までに本番環境】

なお、本番環境でのDBコマンドは下記の通りです。
(デプロイ担当者に捧げます、がんばれ)

ステータス確認
rails db:migrate:status RAILS_ENV=production
データベース完全リセット(currentで行いました)
RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bundle exec rake db:migrate:reset

※私は本番環境で上記の方法しか使ったことがないですが、他に方法があるんでしょうか?

マイグレーション
rails db:migrate RAILS_ENV=production
Seed
rake db:seed RAILS_ENV=production

以上になります!!!

【一部参考にさせていただいたサイト】

Railsガイド Active Record マイグレーション
※ver6.0のページを見ています。

【あとがき】

うちのチームにはDB系のコマンドを交通整理してくれたDB担当メンバーがいるのですが、本当に感謝しかありません。おかげさまでうちのチームはマイグレーションパニックがほとんど起きませんでした。最終課題後少し(多分)

(この記事、大丈夫かな、役に立つかなってドキドキしてます・・・)

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

Rails5でECサイトを作る⑩ ~注文機能を作る~

はじめに

架空のベーカリーで買い物できるECサイトを作るシリーズ、Rails5でECサイトを作る⑨の続きです。このサイトの主要機能であり最も難しい箇所であるOrderモデル(注文機能)周辺を実装します。

ユーザの動きは以下のようになります。

カートに商品を入れて「注文画面に進む」ボタン押下
↓
注文情報入力(orders/new)画面
・支払方法
・お届け先
を入力、「確認画面へ進む」ボタン押下
↓
注文情報確認(orders/confirm)画面
内容を確認して「注文を確定する」ボタン押下
↓
orders/thanks画面を表示(静的ページ)

注文履歴を一覧、詳細画面で確認できる

ポイントは、orders/new画面で入力するお届け先が、「自分の住所」「登録した住所」「新しい住所」から選べることです。新しい住所を選択してnew画面のフォームに内容を入力すると、Orderのデータとして保存されるとともに、Addressモデルにも新規データとして保存されます。
フォームを入れ子にすることと、そのデータを保存する前に確認画面を経由することが、この機能の実装を難しくしている要因です。

ソースコード

https://github.com/Sn16799/bakeryFUMIZUKI

Modelのアソシエーション

fumizuki_ER.jpg

Controller

app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  before_action :authenticate_customer!
  before_action :set_customer

  def index
    @orders = @customer.orders
  end

  def create
    if current_customer.cart_items.exists?
      @order = Order.new(order_params)
      @order.customer_id = current_customer.id

      # 住所のラジオボタン選択に応じて引数を調整
      @add = params[:order][:add].to_i
      case @add
        when 1
          @order.post_code = @customer.post_code
          @order.send_to_address = @customer.address
          @order.addressee = full_name(@customer)
        when 2
          @order.post_code = params[:order][:post_code]
          @order.send_to_address = params[:order][:send_to_address]
          @order.addressee = params[:order][:addressee]
        when 3
          @order.post_code = params[:order][:post_code]
          @order.send_to_address = params[:order][:send_to_address]
          @order.addressee = params[:order][:addressee]
      end
      @order.save

      # send_to_addressで住所モデル検索、該当データなければ新規作成
      if Address.find_by(address: @order.send_to_address).nil?
        @address = Address.new
        @address.post_code = @order.post_code
        @address.address = @order.send_to_address
        @address.addressee = @order.addressee
        @address.customer_id = current_customer.id
        @address.save
      end

      # cart_itemsの内容をorder_itemsに新規登録
      current_customer.cart_items.each do |cart_item|
        order_item = @order.order_items.build
        order_item.order_id = @order.id
        order_item.product_id = cart_item.product_id
        order_item.quantity = cart_item.quantity
        order_item.order_price = cart_item.product.price
        order_item.save
        cart_item.destroy #order_itemに情報を移したらcart_itemは消去
      end
      render :thanks
    else
      redirect_to customer_top_path
   flash[:danger] = 'カートが空です。'
    end
  end

  def show
    @order = Order.find(params[:id])
    if @order.customer_id != current_customer.id
      redirect_back(fallback_location: root_path)
      flash[:alert] = "アクセスに失敗しました。"
    end
  end

  def new
    @order = Order.new
  end

  def confirm
    @order = Order.new
    @cart_items = current_customer.cart_items
    @order.how_to_pay = params[:order][:how_to_pay]
    # 住所のラジオボタン選択に応じて引数を調整
    @add = params[:order][:add].to_i
    case @add
      when 1
        @order.post_code = @customer.post_code
        @order.send_to_address = @customer.address
        @order.addressee = @customer.family_name + @customer.first_name
      when 2
        @sta = params[:order][:send_to_address].to_i
        @send_to_address = Address.find(@sta)
        @order.post_code = @send_to_address.post_code
        @order.send_to_address = @send_to_address.address
        @order.addressee = @send_to_address.addressee
      when 3
        @order.post_code = params[:order][:new_add][:post_code]
        @order.send_to_address = params[:order][:new_add][:address]
        @order.addressee = params[:order][:new_add][:addressee]
    end
  end

  def thanks
  end

  private
  def set_customer
    @customer = current_customer
  end

  def order_params
    params.require(:order).permit(
      :created_at, :send_to_address, :addressee, :order_status, :how_to_pay, :post_code, :deliver_fee,
      order_items_attributes: [:order_id, :product_id, :quantity, :order_price, :make_status]
      )
  end

end

いつものようにform_withで送られた情報をそのまま保存できれば楽なのですが、form_withは確認画面をはさむことができません。そのため、confirm、createアクションではviewから受け取ったパラメータをparams[:hoge]の形で取り出しています。

また、createにおいては一つのアクション内で,「Orderデータを新規作成」「CartItemからOrderItemに商品データを移す(OrderItemの新規作成と登録済みCartItemの削除)」Addressモデル内に一致するデータがない場合のみ新規登録」の3つをこなさなければなりません。一つ一つの動作は複雑ではありませんが、全体のコード量がかなり多くなります。

View

new

app/views/orders/.html.erb
<div class="col-lg-10 offset-1 space">

  <div class="row">
    <div class="col-lg-4">
      <h2>注文情報入力</h2>
    </div>
  </div>

  <%= form_with(model: @order, local: true, url: {action: 'confirm'}) do |f| %>

  <!-- 支払方法 -->
  <div class="row space">
    <h3><strong><%= f.label :支払方法 %></strong></h3>
  </div>

  <div class="row">
    <div class="col-lg-4 btn-group" data-toggle="buttons">
      <label class="btn btn-outline-secondary active" style="width:50%">
        <%= f.radio_button :how_to_pay, true, {checked: true} %> クレジットカード
      </label>
      <label class="btn btn-outline-secondary" style="width:50%">
        <%= f.radio_button :how_to_pay, false, {} %> 銀行振込
      </label>
    </div>
  </div>

  <!-- お届け先 -->
  <div class="row space">
    <h3><strong><%= f.label :お届け先 %></strong></h3>
  </div>
  <!-- 自身の住所 -->
  <div class="row">
    <p>
      <label><%= f.radio_button :add, 1, checked: true, checked: "checked" %>ご自身の住所</label><br>
      <%= @customer.post_code %>
      <%= @customer.address %>
      <%= @customer.full_name %>
    </p>
  </div>

  <!-- 登録済み住所 -->
  <div class="row space-sm">
    <p>
      <label><%= f.radio_button :add, 2, style: "display: inline-block" %>登録住所から選択</label><br>
      <%= f.collection_select :send_to_address, @customer.addresses, :id, :address %>
    </p>
  </div>

  <!-- 新しい住所 -->
  <div class="row space-sm">
    <p><label><%= f.radio_button :add, 3 %>新しいお届け先</label></p>
  </div>
  <div class="row">
    <div class="col-lg-12">
      <%= f.fields_for :new_add do |na| %>
      <div class="row">
        <div class="col-lg-3">
          <strong>郵便番号(ハイフンなし)</strong>
        </div>
        <div class="col-lg-6">
          <%= na.text_field :post_code, class: 'form-control' %>
        </div>
      </div>

      <div class="row">
        <div class="col-lg-3">
          <strong>住所</strong>
        </div>
        <div class="col-lg-6">
          <%= na.text_field :address, class: 'form-control' %>
        </div>
      </div>

      <div class="row">
        <div class="col-lg-3">
          <strong>宛名</strong>
        </div>
        <div class="col-lg-6">
          <%= na.text_field :addressee, class: 'form-control' %>
        </div>
      </div>
      <% end %>
    </div>
  </div>
  <!-- お届け先ここまで -->

  <div class="row space">
    <div class="col-lg-2 offset-lg-7">
      <%= f.submit "確認画面へ進む", class: "btn btn-danger"%>
    </div>
  </div>

  <% end %>
</div>

confirm

app/views/orders/.html.erb
<div class="col-lg-10 offset-1 space">
  <div class="row">
    <h2>注文情報確認</h2>
  </div>

  <%= form_with(model: @order, local: true) do |f| %>

  <div class="d-none d-lg-block space">
    <div class="row">
      <div class="col-lg-5"><h4>商品名</h4></div>
      <div class="col-lg-2"><h4>単価(税込)</h4></div>
      <div class="col-lg-2"><h4>数量</h4></div>
      <div class="col-lg-2"><h4>小計</h4></div>
    </div>
  </div>

  <% sum_all = 0 %>
  <% @cart_items.each do |cart_item| %>
  <div class="row space-sm">
    <div class="col-lg-3">
      <%= link_to product_path(cart_item.product) do %>
      <%= attachment_image_tag(cart_item.product, :image, :fill, 100, 100, fallback: "no_img.jpg") %>
      <% end %>
    </div>
    <div class="col-lg-2">
      <%= link_to product_path(cart_item.product) do %>
      <%= cart_item.product.name %>
      <% end %>
    </div>
    <div class="col-lg-2">
      <%= price_include_tax(cart_item.product.price) %>
    </div>
    <div class="col-lg-2">
      <%= cart_item.quantity %>
    </div>
    <div class="col-lg-2">
      <%= sum_product = price_include_tax(cart_item.product.price).to_i * cart_item.quantity %><% sum_all += sum_product %>
    </div>
  </div>
  <% end %>

  <div class="row space">
    <div class="col-lg-12">
      <div class="row">
        <div class="col-lg-3">
          <strong>送料</strong>
        </div>
        <div class="col-lg-3">
          <%= @order.deliver_fee %></div>
      </div>
      <div class="row">
        <div class="col-lg-3">
          <strong>商品合計</strong>
        </div>
        <div class="col-lg-3">
          <%= sum_all.to_i %></div>
      </div>
      <div class="row">
        <div class="col-lg-3">
          <strong>ご請求額</strong>
        </div>
        <div class="col-lg-3">
          <% billling_amount = sum_all + @order.deliver_fee.to_i %>
          <%= billling_amount.to_i %></div>
      </div>
    </div>
  </div>

  <div class="row space-sm">
    <div class="col-lg-2">
      <h3>支払方法</h3>
    </div>
    <div class="col-lg-4">
      <%= how_to_pay(@order.how_to_pay) %>
    </div>
  </div>

  <div class="row space-sm">
    <div class="col-lg-2">
      <h3>お届け先</h3>
    </div>
    <div class="col-lg-4">
      <%= @order.post_code %>
      <%= @order.send_to_address %>
      <%= @order.addressee %>
    </div>
  </div>

  <%= f.hidden_field :customer_id, :value => current_customer.id %>
  <%= f.hidden_field :post_code, :value => "#{@order.post_code}" %>
  <%= f.hidden_field :send_to_address, :value => "#{@order.send_to_address}" %>
  <%= f.hidden_field :addressee, :value => "#{@order.addressee}" %>
</div>

<div class="row space">
  <div class="col-lg-2 offset-lg-5">
    <%= f.submit "購入を確定する", class: "btn btn-danger btn-lg" %>
  </div>
</div>


<% end %>
</div>

thanks

app/views/orders/.html.erb
<div class="col-lg-10 offset-1 space">
  <h2>ご購入ありがとうございました!</h2>
  <h3><%= link_to 'TOPへ戻る', customer_top_path %></h3>
</div>

「購入を確定する」ボタンを押した後に遷移するページです。静的なページでこれといった機能はないので、上記のように質素な文言だけ載せるも良し、スライダーなどで写真を表示するも良しです。

index

app/views/orders/.html.erb
<div class="col-lg-10 offset-1 space">

  <div class="row">
    <h2>注文履歴一覧</h2>
  </div>

  <div class="d-none d-lg-block space">
    <div class="row">
      <div class="col-lg-2">注文日</div>
      <div class="col-lg-3">配送先</div>
      <div class="col-lg-2">注文商品</div>
      <div class="col-lg-2">支払金額</div>
      <div class="col-lg-1">状況</div>
      <div class="col-lg-2">注文詳細</div>
    </div>
  </div>

  <% @orders.each do |order| %>
  <div class="row space-sm">
    <div class="col-lg-2">
      <%= simple_time(order.created_at) %>
    </div>
    <div class="col-lg-3">
      <div class="row">
        <%= order.post_code + " " + order.send_to_address %>
      </div>
      <div class="row">
        <%= order.addressee %>
      </div>
    </div>
    <div class="col-lg-2">
      <% sum_all = 0 %>
      <% order.order_items.each do |order_item| %>
      <%= order_item.product.name %><br>
      <% sub_total = price_include_tax(order_item.order_price).to_i * order_item.quantity %>
      <% sum_all += sub_total.to_i %>
      <% end %>
    </div>
    <div class="col-lg-2">
      <%= sum_all += order.deliver_fee.to_i %></div>
    <div class="col-lg-1">
      <%= order_status(order) %>
    </div>
    <div class="col-lg-2">
      <%= link_to '表示する', order_path(order), class: "btn btn-sm btn-danger" %>
    </div>
  </div>
  <% end %>

</div>

show

app/views/orders/.html.erb
<div class="col-lg-10 offset-1 space">
  <div class="row">
    <h2>注文履歴詳細</h2>
  </div>

  <div class="row">
    <div class="col-lg-7">
      <div class="row space">
        <h3>注文情報</h3>
      </div>
      <div class="row">
        <div class="container">
          <div class="row space-sm">
            <div class="col-lg-3">
              <strong>注文日</strong>
            </div>
            <div class="col-lg-9">
              <%= simple_time(@order.created_at) %>
            </div>
          </div>
          <div class="row space-sm">
            <div class="col-lg-3">
              <strong>配送先</strong>
            </div>
            <div class="col-lg-9">
              <%= @order.send_to_address %>
            </div>
          </div>
          <div class="row space-sm">
            <div class="col-lg-3">
              <strong>支払方法</strong>
            </div>
            <div class="col-lg-9">
              <%= how_to_pay(@order.how_to_pay) %>
            </div>
          </div>
          <div class="row space-sm">
            <div class="col-lg-3">
              <strong>状況</strong>
            </div>
            <div class="col-lg-9">
              <%= order_status(@order) %>
            </div>
          </div>
        </div>
      </div>

      <div class="row space">
        <h3>注文内容</h3>
      </div>
      <div class="d-none d-lg-block">
        <div class="row">
          <div class="col-lg-4">
            <strong>商品</strong>
          </div>
          <div class="col-lg-3">
            <strong>単価(税込)</strong>
          </div>
          <div class="col-lg-2">
            <strong>個数</strong>
          </div>
          <div class="col-lg-2">
            <strong>小計</strong>
          </div>
        </div>
      </div>
      <% sum_all = 0 %>
      <% @order.order_items.each do |order_item| %>
      <div class="row space-sm">
        <div class="col-lg-4">
          <%= order_item.product.name %>
        </div>
        <div class="col-lg-3">
          <%= price_include_tax(order_item.order_price) %>
        </div>
        <div class="col-lg-2">
          <%= order_item.quantity %></div>
        <div class="col-lg-2">
          <%= sub_total = price_include_tax(order_item.order_price).to_i * order_item.quantity %><% sum_all += sub_total %>
        </div>
      </div>
      <% end %>
    </div>

    <div class="col-lg-5">
      <div class="row space">
        <h3>請求情報</h3>
      </div>
      <div class="row">
        <div class="container">
          <div class="row space-sm">
            <div class="col-lg-6">
              <strong>商品合計</strong>
            </div>
            <div class="col-6">
              <%= sum_all %></div>
          </div>
          <div class="row space-sm">
            <div class="col-lg-6">
              <strong>配送料</strong>
            </div>
            <div class="col-lg-6">
              <%= @order.deliver_fee %></div>
          </div>
          <div class="row space-sm">
            <div class="col-lg-6">
              <strong>ご請求額</strong>
            </div>
            <div class="col-lg-6">
              <%= sum_all + @order.deliver_fee.to_i %></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

orders_show.jpg

上図において、「請求情報」欄に商品の合計額を表示したいのですが、上から順番に画面を構築すると「請求情報」と「注文内容」でそれぞれループを回す必要が出てきて二度手間になってしまいます。そこで、ズルいやり方だよなと思いつつ、colで画面を縦に2分割してまず「注文情報」「注文内容」の欄を作り、「注文内容」のループ内でついでに合計額を計算して変数に格納し、「請求情報」欄で変数を呼び出す、ということをしています。
(「請求情報」欄において、DB内で配送料のdefault値を設定し忘れたので商品合計とご請求額が同じ金額になっています。本来は商品合計に配送料800円がご請求額に加算される仕様で、viewのコードもそれに対応したものとなっています。)

後記

このECサイトの中枢機能だけあって、コード量も多いし内容も複雑でしたね。今回の実装では、おおよその部分では以前私がスクールのチーム実装で作ったものを踏襲しつつ、ヘルパーやモデルのメソッドを新たに定義してコードを見やすくしたり、画面をレスポンシブ対応にしたりといった改変を加えました。

createアクションで新しい住所を選択したらOrderと同時にAddressにも住所データが登録される仕様について追記です。上記の記述ではAddressモデルのaddressカラムを検索して、一致するものがなければ登録する流れになっています。より正確な検索をするなら、

unless Address.find_by(addressee: @order.addressee, address: @order.send_to_address).exists?

として、宛名・住所ともに一致するかを確かめても良いかも知れません。コードは余計に長くなりますが。

何はともあれ、これでcustomerサイトの機能は揃いました。めでたしめでたし。

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

【Rails】form_withに記述するlocal: trueについて

なぜlocal: trueを記述するのか

railsでフォームを送信する際にform_with以下にlocal: trueを記述するが、
最初は意味も分からずにそういうのなんだろうと深く考えもせずに真似てコードを記述していました。

form_with.png

しかし意味も分からずに学習を進めるのは気持ち悪いため改めて調べて見たところ、
rails5においてはlocal: trueを記述しないとajax通信によるフォームの送信という意味になってしまうためHTMLとしてフォーム送信をする場合はlocal: trueの記述が必要とのことです。

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

ActionController::UrlGenerationErrorを解決したお話

解決したいこと

トップページ作成中にマイページへアクセスしようとした際に
https://gyazo.com/979b656dfec9fe3815acef2cd0582933
こちらのエラーが発生

        %li.Header-nav__listsRight--mypage
          = link_to  "マイページ", user_path


こちらのuser_pathに問題がある模様

user_pathに現在ログイン中のユーザーを取得させればよかった。

解決法

         = link_to  "マイページ", user_path(current_user)

path指定の後に(current_user)をつければ良いとのことでした

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

macOS CatalinaにRailsをインストールする

はじめに

macOS CatalinaにRailsをインストールする方法です。
Railsチュートリアルなどでローカル環境にセットアップする場合は、ご参考ください。

前提条件

  • OS: macOS Catalina
  • バージョン: 10.15.5
  • Ruby: 2.7.1
  • Rails: 5.1.6

インストール手順

1. Command line tools をターミナルからインストール:
 xcode-select --install
2. Homebrewをインストール:
 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
3. rbenvをインストール:
 brew update
 brew install rbenv
 echo 'eval "$(rbenv init -)"' >> ~/.zshrc
 source ~/.zshrc
 ※macOS Catalinaからbashからzshに変更になっています。
4. rbenvを使ってRubyをインストール:
 rbenv install 2.7.0
5. デフォルトのRuby を設定:
 rbenv global 2.7.0
6. 必要なソフトウェアをインストール:
 brew install yarn
7. Railsのインストール:
 gem install rails -v 5.1.6
 ※ターミナルを再起動しないとデフォルトのRubyが呼び出されてエラーになる可能性があります。

参考

Rails Girls インストール・レシピ

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

1つの商品に複数画像を登録。ちょっと何言ってるか、わかる!!~part3~

前提

  • ruby on rails 6.0.0 を使用。
  • ユーザー機能はdeviseにより導入されているものとする。
  • viewファイルは全てhaml形式とする。
  • ちなみに使っているのはMacBook Air(Retina, 13-inch, 2020)です。

はじめに

前回(part2)のあらすじです。carrierwaveのuploaderを使って商品登録の際に画像を投稿できる機能を実装しました。

前置きや手順などは part1 に詳しく記載してあるので気になったら騙されたと思ってクリックしてLGTMを押してみましょう。特に何も起こりません。

前回で画像1枚のみの投稿はできるようになったので今回は最大5枚まで一気に登録できるようにしていきます。手順の3番目にあたる、 「jQueryを導入して複数画像の投稿を実装」ですね。

1つ画像を登録するのに3万年かかったのに5つ登録しようと思ったらそれはもう15万年かかるのでは?(あほ)

そんなことはさておき、

画像を複数投稿する

前置きが長くなってしまいましたが、尻込みしていても仕方がないのでさっそくやっていきましょう。

現在の状態だと、画像のフォームは一つしか表示されておらず、一つ選択してしまったらそれで終わりとなってしまっています。そこで登場するのがjavascript。(今回はjQueryを使います)
最終的に、一つ画像を入力したら新しいフォームが出現し、最大5回まで画像を登録できるといった状態ができればゴールとします。

jQueryについて

まずはjQueryの導入からですが、Rails6において、私の知る限りjQueryの導入方法には2つの方向性があります。

  • gemとしてjQueryをインストールする方法
  • webpackerを通してjQueryを呼び出す方法

今回は先述のgemを通した方法でやりました。混乱する人も多いと思うのでjQueryの導入方法は番外編として後ほど投稿しようと思います。というか僕が大混乱しました。

時を飛ばそう

というわけで、無事jQueryが導入できたていで進めていきましょう。

app/views/products/_form.html.haml
= form_with model: @product, local: true do |f|
  = f.text_field :name, placeholder: 'name'
  #image-box
    = f.fields_for :images do |i|
      .group{ data: { index: i.index } }
      = i.file_field :src, 'file'
      %br
      %span.remove 削除
  = f.submit 'SEND'

まずはformのビューファイルにクラスやIDをつけていきます。{ data: ~ } の部分はカスタムデータ属性の指定です。他は特に問題ないと思います。基礎的なことをhamlで書いているだけです。

app/assets/javascripts/product.js
$(function() {
  const buildFileField = (index)=> {
    const html = `<div data-index="${index}" class="group">
                    <input class="file" type="file"
                    name="product[images_attributes][${index}][src]"
                    id="product_images_attributes_${index}_src"><br>
                    <div class="remove">削除</div>
                  </div>`;
    return html;
  }

  let fileIndex = [1,2,3,4,5];

  $('#image-box').on('change', '.file', function(e) {
    $('#image-box').append(buildFileField(fileIndex[0]));
    fileIndex.shift();
    fileIndex.push(fileIndex[fileIndex.length - 1] + 1)
  });

  $('#image-box').on('click', '.remove', function() {
    $(this).parent().remove();
    if ($('.file').length == 0) $('#image-box').append(buildFileField(fileIndex[0]));
  });
});

こちらがjQueryの記述となります。番外編で詳しく書きますが、Rails6のassetsフォルダにjavascriptはないので、自分で作成していただく必要があります。

それでは順を追って解説していきましょう。

let fileIndex = [1,2,3,4,5];

$('#image-box').on('change', '.file', function(e) {
  // ファイルが選択されたときfileIndexの最初の数字をindexとして持ったフォームを新しく作成する。
  $('#image-box').append(buildFileField(fileIndex[0]));
  // fileIndexの最初の数字を削除して数字をひとつずつ左へずらす。
  fileIndex.shift();
  // fileIndexの最後の数字に1を足した数字を最後尾に挿入する。
  fileIndex.push(fileIndex[fileIndex.length - 1] + 1)
  });

まずはこちら、表示されているフォームで画像を選択した際に、新しいフォームが表示される。といった処理ですね。一行ずつの考え方を追記しておきました。これを繰り返すことで、ひとつ一つのフォームに固有のindexを持たせることができます。

const buildFileField = (index)=> {
    const html = `<div data-index="${index}" class="group">
                    <input class="file" type="file"
                    name="product[images_attributes][${index}][src]"
                    id="product_images_attributes_${index}_src"><br>
                    <div class="remove">削除</div>
                  </div>`;
    return html;
  }

先ほどの buildFileField の処理を記述したものです。先ほど作った固有のindexを引数として渡しています。``で囲われている部分は、_form.html.haml で記述されていたフォームをhtmlとして記述し直し、あとで参照できるようにidとnameをつけているだけです。

$('#image-box').on('click', '.remove', function() {
  // クリックされた.removeの親要素を削除する。
  $(this).parent().remove();
  // フォームの数が0になった際、新しいフォームを表示させる。
  if ($('.file').length == 0) $('#image-box').append(buildFileField(fileIndex[0]));
});

最後にフォームを削除する処理ですね。こちらも一行ずつ書いておきました。

さて、ここまでで登録画面における処理はひとまずできました。jQueryの書き方さえ分かっていれば特に難しいこともなかったかと思います。
そういえばfileやremoveなど、一部メソッドなのかクラスなのかがわかりにくかったかもしれませんが、'.~'という形で記述されているのは全てクラスになります。慣れれば簡単に見分けられます。

最後に

大袈裟な前置きをしていましたが、実はデータベースへの複数登録自体は前回で終わっているのです。なので今回はフロントにおける作業がメインでしたね。

ようやく形になってきました。今回で画像の複数登録はできたので、次のpartでは複数画像の編集を行っていきたいと思います。

ではまた次のpartでお会いしましょう。

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

[Rails]ransackで関連するモデル(親や子)のカラムをまたいで検索する方法

実現したいこと

1つのモデルに関連(ネスト)するモデルのカラムまで、検索対象にしたい

具体的には、古着屋の店舗名だけでなく、エリア名(1対多)や取り扱いブランド名(多対多)まで含めて一括検索したい。

スクリーンショット 2020-07-11 17.45.47.png

結論

フォームタグの要素名に、関連するモデル名_関連するモデルのカラム名を指定する

関連するモデルが、対1(belongs_to: hoge、やhas_one: fuga)

例えば、shopモデルに紐づくareaモデルのエリア名(name)を検索条件にしたい時
= f.フォームヘルパー :要素名要素名area_name_contとする。

分解すると
area → 関連するモデル名
name → 関連するモデルのカラム名
cont → 部分一致を指定する述語

となります。

= search_form_for(@q, url: shop_search_path) do |f|
  = f.text_field :area_name_cont
  # shopモデルに紐づくareaモデルの、エリア名(name)

関連するモデルが、対多(has_many: hogesなど)

例えば、shopモデルに紐づくbrandsモデルのブランド名(name)を検索条件にしたい時
= f.フォームヘルパー :要素名要素名brands_name_contとする。

分解すると
brands → 関連するモデル名 ※shop has_many: brandsなので複数形
name → 関連するモデルのカラム名
cont → 部分一致を指定する述語

となります。

= search_form_for(@q, url: shop_search_path) do |f|
  = f.text_field :brands_name_or_genres_name_cont
  # shopモデルに紐づくbrandモデルの、ブランド名(name)
  # shopモデルに紐づくgenreモデルの、ジャンル名(name)
  # ※紐づくモデルが複数の時は、モデル名が複数形になることに注意

ちなみに、_or_などでカラム名を繋ぐと、複数カラムを検索対象にできます。

実際のransackの使い方などについては、[Rails]ransackを利用した色々な検索フォーム作成方法まとめなどの記事を参考にしてください。

モデル間のアソシエーション

※関連する箇所のみ記載

スクリーンショット 2020-07-11 18.17.14.png

shop.rb
# shopモデル
  belongs_to :area, optional: true
  has_many :shop_genres
  has_many :shop_brands
  has_many :genres, through: :shop_genres
  has_many :brands, through: :shop_brands
area.rb
# areaモデル
  has_many :shops
brand.rb
# brandモデル
  has_many :shop_brands
  has_many :shops, through: :shop_brands
shop_brand.rb
# shop_brandモデル
  belongs_to :shop
  belongs_to :brand
shop_genre.rb
# shop_genreモデル
  belongs_to :shop
  belongs_to :genre

関連を調べる方法

ransackable_associationsというメソッドを使うと便利です。

1. アプリケーションディレクトリでrails c

terminal
# 該当のアプリケーションディレクトリで実行

$ rails c
Running via Spring preloader in process 61541
Loading development environment (Rails 5.0.7.2)
[1] pry(main)> 

2. モデル名.ransackable_associationsを実行

terminal
# 今回はShopモデルとの関連を調べたいので、Shop.ransackable_associationsとすると
  Shopモデルに紐づくモデルが表示される
[1] pry(main)> Shop.ransackable_associations
=> ["user", "area", "shop_genres", "shop_brands", "genres", "brands"]
[2] pry(main)> 

参考

Ransackで簡単に検索フォームを作る73のレシピ -026 関連
Ransackで親テーブルや子テーブルのカラムで複数検索する方法

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

【随時更新】Ruby on Rails 便利なメソッド

alias化されたattributeを知りたい。

pry(main)> Member.attribute_aliases
=> { "user_name"=>"UserName", "mail_address"=>"MailAddress",}

methodsメソッドで取得したメソッド一覧からgrepで検索したい。

pry(main)> Member.methods.grep /attribute_aliases/i
=> [:attribute_aliases, :attribute_aliases?, :attribute_aliases=]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsにgRPCクラアント導入

経緯

会社でPMを担当しているプロダクト(Rails)の実装で他のサービスから情報を取得する必要があり、そのサービスがOpenAPIでなく、gRPCでAPIインタフェースが定義されていたので、gRPCのクライアントを実装しました。
僕が前職でrailsでのgRPC周りを少し触っていたので、相対的に僕がやった方がチームの開発工数が増えると思ったので自分でやることにしました。
それについてのアウトプットです。

実装手順

1. gem導入

※前職で使っていたgrufというライブラリを使いました。

Gemfile
gem "google-protobuf" 
gem "grpc-tools"
gem "gruf"

※ webというコンテナで開発してる

docker-compose run --rm web bundle install
2. protoファイルが管理されているリポジトリをsubmoduleでアプリケーションのサブディレクトリとして登録して、protoファイルを元のリポジトリから取ってくる
$ git submodule add [web URL or ssh key] proto
$ git submodule init
$ cd proto
$ git submodule update
3. protoファイルをrubyファイルにコンパイル
docker-compose run --rm web grpc_tools_ruby_protoc -I [コンパイル対象のprotoディレクトリ] --ruby_out=[コンパイル後のファイル保存先のディレクトリ] --grpc_out=[コンパイル後のファイル保存先のディレクトリ] [コンパイル対象のprotoディレクトリ内の対象ファイル]
4. コンパイル後のrubyファイル(*_pb.rb)の読み込み設定

コンパイル後のファイルは、ファイル名と、クラス名が噛み合っておらず、Railsの読み込み規則に則っていなく自動読み込まれないので、指定してあげる必要がある。

config/initializers/gruf.rb
require "gruf"

Gruf.configure do
  Dir.glob(Rails.root.join("[コンパイル後のファイル保存先のディレクトリ]/*_pb.rb")).each do |file|
    require file
  end
end

コンパイル後のファイルでは下記のように自動で指定されており、auto_load_pathに追加しておく必要がある。
e.g. gruf-demoから
require 'Products_pb'

※コンパイル後のファイルは基本修正しないので。

config/application.rb
  class Application < Rails::Application
    config.paths.add [コンパイル後のrubyファイルディレクトリ], eager_load: true
  end
5. クライアントがサーバをコールする部分の実装

ここまでで、全てのコンパイル後のrubyファイルは使えるようになったでのクライアントの実装。

moduleにしようかとか悩みましたが、既存の実装でクライアント系の処理はservice層にまとめていたので、今回もそれに習う形にしました。

※特にmetadata周りはサーバ側の実装に依存するので注意。
grufのwikiだと、クライアントの初期化(Gruf::Client.new)時のoptions引数のキーでusernameを入れていたりとこの辺が今回の実装と違っており、若干悩みました。

app/services/grpc_client_service.rb
class GrpcClientService
  def initialize
    @metadata = {
      login: ENV["GRPC_CLIENT"],
      password: ENV["GRPC_PASSWORD"]
    }
  end

  def run(service_klass, method, request)
    client = Gruf::Client.new(
      service: service_klass,
      options: {
        hostname: ENV["GRPC_HOST"],
        channel_credentials: :this_channel_is_insecure
      }
    )

    client.call(method, request.to_h, @metadata)
  end
end

導入してみての感想とか

前職でgrufは使っていたので、余裕かと思っていましたが、やはり導入するのと、ただ使うだけでは結構違うなと思いました。
しっかり設定周りのコードを読んでおけば良かったと後悔してます。

また、今回gRPCサーバがgoで書かれており、クライアントからのコールがうまく行かないときにコード読むのに苦労して結局諦めたので、goも勉強したいなと思いました。

また少し詰まったのは、クライアントの初期化(Gruf::Client.new)時にmetadataを入れると謎の勘違いしており(本当はcall時の引数に入れる)、ライブラリのWikiに書いてないことはやはり、しっかりコード読まないといけないなと初歩的なことを改めて実感しました。

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

ridgepole [ERROR] uninitialized constant Rails (erb):16:in `<main>'

忘備録
自分用とはいえだいぶ読みにくいと思います。すみません。

intro

いつものように開発環境コマンドプロンプトにて。
bundle exec ridgepole --config ./config/database.yml --file ./db/Schemafile --apply --dry-run
もしくは
bundle exec ridgepole --config ./config/database.yml --file ./db/Schemafile --apply
した時。

[ERROR] uninitialized constant Rails
        (erb):16:in `<main>'

???? どこの何がどうエラーなん!?!?
と困惑。エラーメッセージは「uninitialized constant Rails (erb):16:in `'」
だけでNameErrorとかも何も教えてくれなかったので。

調べた感じ、とりあえず初期化時にクラスやファイルの読み込みが上手く行ってなかったりなんだな~
と思ったので、前に上手く行った段階と今回でファイルをいじったなって所をコメントアウトやら色々試していった。

+元々こちらの記事を読んだことがあったので→ ridgepole 導入はまりどころ
自分で試しているうちに
「そういえばdatabase.ymlもいじって、

database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= Rails.application.credentials.mysql[:username] %> ?ココ
  password: <%= Rails.application.credentials.mysql[:password] %>  ?ココ
  host: localhost

とかで展開してたな......」と。
dotenvじゃないですが、開発環境でcredentials使っているので(本来は本番環境だけで使うもの)
これだなと、--apply時にうまく展開されてないんだなと。
16行目でエラー出てたけど、まさにこの場所でした。

試しに

database.yml
username: testuser
password: testpass1111

と展開なしで直接書いたらバッチリ動きました。

ついでにdotenvを使うことにした

dotenv自体の使い方については
Railsで使える環境変数を管理できるgem(dotenv-rails)や.envの導入方法
を参考にしつつ。

ついでに気になったので下記の記事も読みました。
ENV[]とENV.fetch()の違い【Rails/Ruby】
gitにdatabase.ymlはあげて管理したいので、fetchの方を使ってデフォルト値も入力してしまうと
結局見えちゃって意味ないな~と思い
出力するときはENV.fetchじゃなく普通にENV['']使いました。

development:
  <<: *default
  database: skuemy_system_development
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>

として読み込むようにしました。

しかし

ymlファイルのコメントアウトがどうも上手く働いてくれないままなんですが。
上記のように変えて、
下の方に

database.yml
# username: <%= Rails.application.credentials.mysql[:username] %>
# password: <%= Rails.application.credentials.mysql[:password] %>

と下の方にメモ書きとして残しておいたのです。ですが、
コメントアウトしているにもかかわらず読み込もうとするんです。(未解決)

なので
bundle exec ridgepole --config ./config/database.yml --file ./db/Schemafile --apply --dry-run
とかしても

[ERROR] uninitialized constant Rails
        (erb):57:in `<main>'

ってさっきコメントアウトしていた行、57行目でダメーって言われました。
なのでメモは別でとっておくとして、消しました。そうすると無事に動......

きません。

[ERROR] Access denied for user 'ユーザー名'@'localhost' (using password: NO)

とでます。つまるところデータベースへのアクセス。usernameとpasswordが間違ってるか無いよって言われます。
dotenvで展開してるのにナンデ!?なのですが。
下記の記事を読んでいたので。(2回目)
ridgepole 導入はまりどころ ==> 案3 コマンド実行時に即時展開

案3にあるコマンド実行時に即時展開というのを少し編集して使うことにします。

bundle exec dotenv -f ".env" ridgepole -c config/database.yml -E development -f db/Schemafile --apply --dry-run

ファイルの場所は任意なので変えるのは当然として
splitしたくなかったり、
そもそもファイルは既に作ってあるので--export とか--outputは使いませんでした。

「案4 rake task内から間接的に」
も動きました。実際に使っているコード一応貼っておきます。
ファイル名は適当です。
編集したのはファイルの参照部分だけです。

task_ridgepole_apply.rake
namespace :db do
  desc 'apply Schemafile and update schema.rb'
  task apply: :environment do
    ENV['ALLOW_DROP_TABLE'] ||= '0'
    ENV['ALLOW_REMOVE_COLUMN'] ||= '0'
    ENV['RAILS_ENV'] ||= 'development'
    task_return = `ridgepole -E #{ENV['RAILS_ENV']} --diff config/database.yml db/Schemafile`
    column_condition = task_return.include?('remove_column') && ENV['ALLOW_REMOVE_COLUMN'] == '0'
    table_condition = task_return.include?('drop_table') && ENV['ALLOW_DROP_TABLE'] == '0'
    if column_condition || table_condition
      puts '[Warning]this task contains some risks: "remove_column" or "drop_table"'
    else
      sh "ridgepole -E #{ENV['RAILS_ENV']} -c config/database.yml --apply -f db/Schemafile"
      sh 'rake db:schema:dump'
    end
  end
end

これで誤ってテーブルが削除されたりするリスクがだいぶ減りそうです。
Rakeタスク内でリスクを担保する
感謝です。ほんとに。


まとめ(れてない)

ymlの一部コメントアウトが効かないのは今の所1mmも意味が分かりませんが、
そのコメント部分を消して。
「bundle exec dotenv -f ".env" ridgepole -c config/database.yml -E development -f db/Schemafile --apply --dry-run」
(dry-runを付ける場合の話)
で動いているのでヨシとします。(現場猫)


余談ですが、

database.yml
#  username: <%= ENV['DATABASE_USERNAME'] %>
#  password: <%= ENV['DATABASE_PASSWORD'] %>

と、dotenvで展開していた方をコメントアウトしたらちゃんと(passとかが無いよって)エラーでてくれたので。
そっちはコメントアウト効いているようです。
もうよくわかりません。
Rails.application.credentialsに何かあるとしか......。

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