20200222のRailsに関する記事は25件です。

【Rails】herokuでS3に画像をアップロード(fog)

Railsチュートリアル第13章でheroku → S3デプロイまで苦戦したためご参考先とともにメモ。
本記事に表現・内容の誤り等あればご指摘頂けると幸いです(^_^;)

エラー

・heroku→S3へ画像投稿ができない(マイクロポストテキストはできる)
・(投稿成功後)画像のリサイズで小さくならない

※画像がheroku内かS3どうかは、マイクロポストでアップした画像を右クリックし、
画像アドレスをコピーでリンク先が「 ~s3.amazonaws~ 」になっていればok。

対処・間違っていたこと等

・IAM→S3作成ユーザー(≠これまでのEC2作業ユーザ)
・バケットポリシー変更(✔︎新ポリシーのみブロック≠全ブロック)
・uploaderのlimitサイズ変更で確認(そもそも投稿元のサイズが縦長で大きく見えただけだった)

※ 投稿後にS3を確認すると、uploadsフォルダが作成されている

< 流れ >

  1. S3(バケット)作成
  2. EC2作業ユーザとは別に新規IAMユーザ作成(Fullアクセス権限付与、バケットからAccessキー & Secretキー取得のため)
  3. パプリックアクセス一時的に解除 & ポリシー付与
  4. 再度パブリックアクセス(新規条件のブロックに✔︎)
  5. herokuでS3アクセスキー等追加

S3パブリック設定


パブリックアクセスをすべてブロック
オフ

新しいアクセスコントロールリスト (ACL) を介して許可されたバケットとオブジェクトへのパブリックアクセスをブロックする
オフ

任意のアクセスコントロールリスト (ACL) を介して許可されたバケットとオブジェクトへのパブリックアクセスをブロックする
オフ

新規のパブリックバケットポリシーまたはアクセスポイントポリシーを介して付与されたバケットとオブジェクトへのパブリックアクセスをブロックする
オン

任意のパブリックバケットポリシーまたはアクセスポイントポリシーを介したバケットとオブジェクトへのパブリックアクセスとクロスアカウントアクセスをブロックする
オフ


各種作成設定ファイル

carrier_wave

config/initializers/carrier_wave.rb
if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_provider = 'fog/aws'  #=> 追加
    config.fog_credentials = {
      # Amazon S3用の設定
      :provider              => 'AWS',
      :region                => ENV['S3_REGION'],     # 例(東京): 'ap-northeast-1'
      :aws_access_key_id     => ENV['S3_ACCESS_KEY'],
      :aws_secret_access_key => ENV['S3_SECRET_KEY']
    }
    config.fog_directory     =  ENV['S3_BUCKET']
    # キャッシュの保存期間
    config.fog_attributes = { 'Cache-Control' => "max-age=#{365.day.to_i}" }
  end
  # 日本語ファイル名の設定
  CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/ 
end

 
store_dirはデフォルトの保存先(S3で確認すると出てくる)

app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  process resize_to_limit: [300, 300]

  # Choose what kind of storage to use for this uploader:
  if Rails.env.production?
    storage :fog
  else
    storage :file #=> ローカルストレージ
  end
  # storage :fog

  # アップロードファイルの保存先ディレクトリは上書き可能
  # 下記はデフォルトの保存先(S3で確認すると出てくる)
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_whitelist
    %w(jpg jpeg gif png)
  end
end

 
Gemfile

Gemfile
group :production do
  gem 'pg', '0.20.0'
  gem 'fog-aws'#=> fogから変更
end

その他

heroku関係

""は無くてもok(heroku configで確認できる)

$ heroku config #=> 状態確認
$ heroku logs #=> ログ確認
$ heroku config:set S3_ACCESS_KEY="CSVダウンロードしたAccessキー"
$ heroku config:set S3_SECRET_KEY="ダウンロードしたSecretキー"
$ heroku config:set S3_BUCKET_KEY="S3のBucketの名前"
$ heroku config:set S3_REGION="ap-northeast-1"

ご参考先

【公式】AWS Config 開発者ガイド
S3のバケットポリシーでハマったので、S3へのアクセスを許可するPrincipalの設定を整理する
【Rails】S3へ『CarrierWave+fog』を使って画像アップロードする方法
HerokuでS3に画像をアップロードした話[Rails][S3][CarrierWave][fog]
Rails5 heroku にS3を使ってアップロードしたときに発生するエラー 403 Access denied の対処法

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

【AWS】herokuでS3に画像をアップロード(Rails)

Railsチュートリアル第13章でheroku → S3デプロイまで苦戦したためご参考先とともにメモ。
本記事に表現・内容の誤り等あればご指摘頂けると幸いです(^_^;)

エラー

・heroku→S3へ画像投稿ができない(マイクロポストテキストはできる)
・(投稿成功後)画像のリサイズで小さくならない

※画像がheroku内かS3どうかは、マイクロポストでアップした画像を右クリックし、
画像アドレスをコピーでリンク先が「 ~s3.amazonaws~ 」になっていればok。

対処・間違っていたこと等

・IAM→S3作成ユーザー(≠これまでのEC2作業ユーザ)
・バケットポリシー変更(✔︎新ポリシーのみブロック≠全ブロック)
・uploaderのlimitサイズ変更で確認(そもそも投稿元のサイズが縦長で大きく見えただけだった)

※ 投稿後にS3を確認すると、uploadsフォルダが作成されている

< 流れ >

  1. S3(バケット)作成
  2. EC2作業ユーザとは別に新規IAMユーザ作成(Fullアクセス権限付与、バケットからAccessキー & Secretキー取得のため)
  3. S3へ行きアクセス権限からパプリックアクセスを一時的に解除 & ポリシー付与
  4. 再度パブリックアクセス(新規条件のブロックに✔︎)
  5. herokuでS3アクセスキー等追加

S3パブリック設定


パブリックアクセスをすべてブロック
オフ

新しいアクセスコントロールリスト (ACL) を介して許可されたバケットとオブジェクトへのパブリックアクセスをブロックする
オフ

任意のアクセスコントロールリスト (ACL) を介して許可されたバケットとオブジェクトへのパブリックアクセスをブロックする
オフ

新規のパブリックバケットポリシーまたはアクセスポイントポリシーを介して付与されたバケットとオブジェクトへのパブリックアクセスをブロックする
オン

任意のパブリックバケットポリシーまたはアクセスポイントポリシーを介したバケットとオブジェクトへのパブリックアクセスとクロスアカウントアクセスをブロックする
オフ


ポリシー

既にEC2で開発している場合、S3用で作った新規ユーザではIDが出るわけではないので注意(元のものでok)

※ 入力に ""「」は不要

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "statement1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::AWSユーザのID(12桁の数字):user/新規に作ったS3用ユーザ名"
            },
            "Action": "*",
            "Resource": "arn:aws:s3:::バケット名/*"
        }
    ]
}

各種作成設定ファイル

carrier_wave

config/initializers/carrier_wave.rb
if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_provider = 'fog/aws'  #=> 追加
    config.fog_credentials = {
      # Amazon S3用の設定
      :provider              => 'AWS',
      :region                => ENV['S3_REGION'],     # 例(東京): 'ap-northeast-1'
      :aws_access_key_id     => ENV['S3_ACCESS_KEY'],
      :aws_secret_access_key => ENV['S3_SECRET_KEY']
    }
    config.fog_directory     =  ENV['S3_BUCKET']
    # キャッシュの保存期間
    config.fog_attributes = { 'Cache-Control' => "max-age=#{365.day.to_i}" }
  end
  # 日本語ファイル名の設定
  CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/ 
end

 
store_dirはデフォルトの保存先(S3で確認すると出てくる)

app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  process resize_to_limit: [300, 300]

  # Choose what kind of storage to use for this uploader:
  if Rails.env.production?
    storage :fog
  else
    storage :file #=> ローカルストレージ
  end
  # storage :fog

  # アップロードファイルの保存先ディレクトリは上書き可能
  # 下記はデフォルトの保存先(S3で確認すると出てくる)
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_whitelist
    %w(jpg jpeg gif png)
  end
end

 
Gemfile

Gemfile
group :production do
  gem 'pg', '0.20.0'
  gem 'fog-aws'#=> fogから変更
end

その他

heroku関係

""は無くてもok(heroku configで確認できる)

$ heroku config #=> 状態確認
$ heroku logs #=> ログ確認
$ heroku config:set S3_ACCESS_KEY="CSVダウンロードしたAccessキー"
$ heroku config:set S3_SECRET_KEY="CSVダウンロードしたSecretキー"
$ heroku config:set S3_BUCKET_KEY="S3のBucketの名前"
$ heroku config:set S3_REGION="ap-northeast-1"

ご参考先

【公式】AWS Config 開発者ガイド
S3のバケットポリシーでハマったので、S3へのアクセスを許可するPrincipalの設定を整理する
【Rails】S3へ『CarrierWave+fog』を使って画像アップロードする方法
HerokuでS3に画像をアップロードした話[Rails][S3][CarrierWave][fog]
Rails5 heroku にS3を使ってアップロードしたときに発生するエラー 403 Access denied の対処法

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

asset_path について

便利と思ったので備忘録。

asset_path について

画面上にimageを表示する際、
'/assets/titel.png'と記述する事も多いですが、

asset_pathを使えば、

= image_tag asset_path("title.png")
↑で
app/assets/images/title.pngを参照してくれます。
(プロダクション環境でも参照可能)

他にも参考に
application.js というファイルが assets/javascripts に存在する場合
'/assets/application.js' が、=>asset_path('application.js') てな感じ。

image.png というファイルが assets/images に存在する場合
'/assets/image.png'が、=>asset_path('image.png') てな感じ。

config.assets.digest = true な環境で application.css というファイルが assets/stylesheets に存在する場合
'/assets/application817776380c2b21405e00e88cbdd215e1.css'が、=> asset_path('application.css') てな感じ。

参考記事
Railsのasset_pathは何をやってくれてるのか

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

[Rails] ActiveRecordでデータを更新したときに、各カラムが更新されたかどうか知りたかったんだ

はじめに

タイトル通りです。

下のアプリはチャットルームに「タイトル」と「ルームの説明」、「カテゴリ」を設定できるんだけれども、
「タイトル」が変更された時だけトークルームに通知が来るようにしたかった。
Image from Gyazo

変更前のパラメーターを取得するか、変更されたかどうかを判定するメソッドがあったら嬉しいなーと思って探した。

実装

どうやらActiveRecordさんには「カラム名_changed?」っていうヘルパーメソッドがあるらしいと聞いて...

room_controller
  def update
    if @room.update(room_update_params)
      if @room.title_changed? # ココ。title_cahged?がtrueであれば自動メッセージを送るようにしたつもり。
        Message.create(room_id: @room.id, user_id: current_user.id, content: "*自動投稿* タイトルを「#{@room.title}」に編集しました")
      end
      redirect_to room_path(@room.id)
    else
      render :edit
    end
  end

こんな感じで実装してみたのだけれども失敗した。

Githubのrailsを見てみると、なんか更新されているらしい。

つまり

カラム名_changed?は、ActiveRecordのコールバックを使って処理を分けることができていたが、

コールバックまとめ

save前の場合は、

オブジェクト.will_save_change_to_カラム名

save後の場合は、

オブジェクト.saved_change_to_{カラム名}?

という風に書き変える必要があるみたい。

再度実装

room_controller.rb
  def update
    if @room.update(room_update_params)
      if @room.title_updated?
        Message.create(room_id: @room.id, user_id: current_user.id, content: "*自動投稿* タイトルを「#{@room.title}」に編集しました")
      end
      redirect_to room_path(@room.id)
    else
      render :edit
    end
  end

コントローラーで「saved_change_to」やら「will_save_change_to」やらを呼び出すとうまくいかなかったので、
モデル側に「更新されたかどうか判別するインスタンスメソッド」を作りました。

room.rb
  def title_updated?
    if self.saved_change_to_title?
      return true
    else
      return false
    end
  end

これでタイトル編集時のみメッセージが投稿されるようになりました。

Image from Gyazo

おわりに

今回初めてrailsのソースコードを見ましたが、もっと見ていくととても勉強になりそうです。
暇を見つけてもっと読んでみようと思いました。

おわり。

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

Docker-based deployments to heroku of Rails App(postgres) 2020.02

Overview

ここに書くこと

  • Dockerコンテナ上で動作するRailsアプリケーションに関する話です
  • Railsアプリケーションをherokuにデプロイする話です
  • dbはpostgreSQLを使います

Prerequisite

  • 以下の開発環境が出来上がっている
    • railsサーバー, dbサーバー用のdockerコンテナ(postgreSQL)のみ
    • docker-composeでコンテナ同士が協調している
  • ローカルでrailsアプリが正常動作している
    • rails s + ブラウザにてWebページの確認ができる

System

  • Docker
    • ver: 19.03.5, build 633a0ea
  • Docker-compose
    • ver: 1.25.2, build 698e2846
  • heroku CLI
    • ver: 7.36.3 darwin-x64 node-v12.13.0

Table of Contents

  1. Conclusion
  2. -aオプションが必要
  3. dbコンテナはherokuのアドオンを使う
  4. 参考にさせていただいたページ

Main

1. Conclusion

まず、結論です。以下の順にコマンドを打てば良いです。

docker-based-deployments-to-heroku(rails-app)
# webサービスイメージをherokuにpush
heroku container:push web -a myapp
# postgresql用add-onを作成する(大事)
heroku addons:create heroku-postgresql -a myapp
# webサービスコンテナをリリース(heroku上でコンテナを実行する?)
heroku container:release web -a myapp
# heroku上でdbのマイグレーション
heroku run rails db:migrate -a myapp
# デプロイしたWebページを開く
heroku open -a myapp

基本的には、参考にさせていただいたサイト通りなのですが、
いくつかそのままでは実行できない箇所があったので以下記述します。

2. -aオプションが必要

おおよそ全てのコマンドにおいて、
デプロイ対象のインスタンス(herokuではdynoと呼ばれる)の名称を指定する必要があります。
名称は、herokuのマイページでも確認できますし、CLIでheroku appsと入力しても確認できます。

ダメな例
heroku container:push web

と入力すると

エラーメッセージ
 ›   Error: Missing required flag:
 ›     -a, --app APP  app to run command against
 ›   See more help with --help

というエラーメッセージが出るので、-a オプションで指定します。

良い例
heroku container:push web -a myapp

3. dbコンテナはherokuのアドオンを使う

herokuではvolumeが使えません。
したがって、PostgreSQLはherokuのアドオンを利用して、
webコンテナと接続させます。その設定が heroku addons:create heroku-postgresql -a myapp です。

4. 参考にさせていただいたページ

本記事は必要最小限の内容しか記述していません。
以下に有用な情報があります。

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

初めてのチーム開発(フリマサイト)

チーム分けと分担

スクールに通い出して初めてのチーム開発がスタートしました。
チームは5人。みんな現職者です。

初日は全体ミーティングで、チーム分けと、全体の流れの説明がありました。
その後、時間も遅かったので翌日再集合として解散。

翌日に集まり、開発物の確認、ゴールの設定、分担等の話し合いを実地します。

  • 今回の開発物:フリマサイト(メルカリ)のコピーサイト
  • ゴールの設定:最低限の機能実装に留める。(スクール指定)
  • 分担:きちんと決まりませんでした。

分担は正直初めてでは難しかったです。初見の5人の集まりで技術力も不安だらけ、おまけに現職者で時間も厳しい。
それでもスクラムマスターを決めて格機能の見積もりを付けるところに行きました。
とは言え、技術力が無い=実装がどれくらい大変か分からないと言うことです。
とりあえす高、中、低の3段階で最初のスプリントレビューまで(2週間)やってみようとなりました。

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

[取り置き機能]Rails初学者が某フリマアプリにオリジナル機能を実装してみた。

某スクールのチーム開発にて某フリマアプリを作成しております。
先日、必須機能の実装が完了し、追加機能を実装していくフェーズになりました。
そこで、自分なりに色々と考えた結果、タイトルに記載いたしました「取り置き機能」を実装しました!
今回は、機能追加を決めるにあたっての背景含めて以下に記載いたします。

今までは既存のコンテンツの実装であったため、様々な記事や情報があったため、自分で実装コンテンツや実装方法を考えるという機会は少し少なかった印象ありました。
しかしながら、今回はオリジナル機能であったため、実際のアプリユーザのことを考えながら実装をしていくのは、非常に楽しかったです!!

背景(「取り置き機能」を追加機能に選んだ理由)

今回、追加機能を選ぶにあたり、ネットで某フリマアプリユーザのコメントをネットで探したり、自分でサイトをいじっていました。
その結果、以下の状況が見えてきたため、今回の「取り置き機能」実装を決断しました。
- 問題・課題:
特定のユーザに販売する際に、出品写真やタイトルなどに”〇〇様向け”などと書くしかできず、他ユーザに購入できないようにするブロックができない。
その状況のため、別ユーザがユーザ名を偽ることで、本来購入をする予定であったユーザが購入できず、別のユーザに購入されてしまうという問題が発生している。

「取り置き機能」実装に関する考え方

今回の機能実装に関して、以下のように考えました。
1. 商品を出品するユーザが、商品に対して”キー”を設定する。
2. その”キー”を持っているユーザは購入をできるが、”キー”を持っていないユーザは購入をできないようにする。

ここで問題となったのは何を"キー”とするかでした。
「出品するユーザが合言葉を設定し、それを入力させる」や「ユーザ名を”キー”とする」などを考えましたが、どれもユニーク性がなかったためボツとし、今回は購入して欲しいユーザのemailアドレスを”キー”とすることにし,”reservation_email”というカラムを追加することにしました。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。

*実際はemailアドレスを"キー”とするのはプライバシーの観点からあまりよろしくはないと思われますため、可能で有れば、会員番号などを”キー”とするのがベターかと考えます。(今回は会員番号を設定していなかったため、アドレスにしました)

実装内容(イメージ)

今回実装した機能のイメージは以下の通りになってます。

出品ユーザ側機能

機能1:取り置きをする

demo

機能2:取り置きをやめる

demo

機能3:その他ユーザに購入できないようにする

demo

購入ユーザ側機能

機能1:取置き品を購入する

demo
*購入すると以下のように、SOLDとなります。
demo
購入についての実装は、以下の記事をご参照ください。
[HowTo]Pay.jpを用いた商品購入機能実装から商品購入後の設定まで
https://qiita.com/Tatsu88/items/eb420e372077939a4627#%E5%95%86%E5%93%81%E8%B3%BC%E5%85%A5%E7%A2%BA%E8%AA%8D%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E7%B7%A8%E9%9B%86

機能実装:マイグレーションファイル

マイグレーションファイルは以下のように実装してます。
今回は、t.string :reservation_emailを追加してます。
こちらが、取り置き機能を実現するための要となります。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。

マイグレーションファイル
class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name, limit: 191,null:false,index: true
      t.integer :price, index: true
      t.text :explain,null:false
      t.integer :postage,null:false
      t.integer :status
      t.integer :shipping_date
      t.integer :size
      t.integer :brand_id
      t.integer :category_id
      t.integer :prefecture
      t.integer :buyer_id
      t.references :user,index: true, foreign_key: true
      t.string :reservation_email
      t.timestamps
    end
  end
end

機能実装(ルーティング)

ルーティングは以下のように実装してます。
'reserve'、'reserved',patch 'reserve_cancel'を今回追加してます。
内容としては、予約・予約完了・予約取り消しとなります。

ルーティング
  resources :products do
    member do
      post 'purchase'
      get 'purchased'
      get 'buy'
      get 'reserve'
      patch 'reserved'
      patch 'reserve_cancel'
    end
    resources :comments,only:[:create,:destroy]
  end

機能実装(ビュー)

ビューは以下のように実装してます。
基本的に"reservation_email"の情報有無によって表示内容を変えてます。

1. 商品一覧

こちらでは購入済みか取り置き済みかでサムネイルとして表示する写真にラベルを取り付けます。

view
.product__thumbnail--image
 =image_tag (product.images[0].product_image.url)

#購入されていれば、"SOLD"のラベルをつけます。
-if product.buyer_id.present? 
 .items-box_photo__sold
  .items-box_photo__sold__inner SOLD

#取り置きされていれば、"Reserved"のラベルをつけます。
-if product.reservation_email.present? 
 .items-box_photo__reserved
  .items-box_photo__reserved__inner Reserved

2. 商品詳細

こちらではユーザの状態によって、ビューの表示内容を変えてます。

view
.product-buy__btn__box  
#ログインユーザが出品者の場合の、表示内容 
 - if user_signed_in? && current_user.id ==@product.user_id
  = link_to "削除する", product_path(@product.id), method: :delete,class:"product-details-delete__btn"
  = link_to "編集する", edit_product_path(@product.id),class:"product-details-edit__btn"
  = link_to "取り置きする/編集する", reserve_product_path(@product.id),class:"product-details-resorve__btn"

#購入者向けの表示内容
#既に購入されている時
 - elsif @product.buyer_id.present? 
  = link_to "売り切れました",buy_product_path,class:"disabled-button bold"

#取り置きされていて、その取り置きを商品を購入することを許可されているユーザの時
 - elsif @product.reservation_email.present? && @product.reservation_email == current_user.email
  = link_to "取り置き商品を購入する",buy_product_path,class:"product-purchase__btn"

#取り置きされていて、その取り置きを商品を購入することを許可されていないユーザの時
 - elsif @product.reservation_email.present? && @product.reservation_email != current_user.email
  = link_to "取り置き商品のため購入できません",buy_product_path,class:"disabled-button bold"
 - else
  = link_to "購入画面に進む",buy_product_path,class:"product-purchase__btn"

2. 取り置き画面

こちらでは、"reservation_email"というカラムの情報有無によって表示する内容を変えております。
情報が有れば、「取り消す」ボタンが出てきます。

ビュー(取り置き画面)
%main.buy-main 
  .buy-item-container
    %h2.buy-item-head 取り置き内容の確認
    %section.buy-content.buy-item
      .buy-content-inner
        .buy-item-box
          .buy-item-image
            =image_tag(@product.images[0].product_image.url,class:"buy-image")
          .buy-item-detail
            %p.buy-item-name
              =@product.name
              %p.buy-item-price.bold
                = number_to_currency(@product.price,format: "%u%n",unit:"¥",precision: 0)
                %span.item-shipping-fee.f14.bold
                  (税込)送料込み
        =form_for(@product, url: reserved_product_path,method: :patch) do |f|
          .form-group
            =f.label :お取り置きをする方のアドレス
            %span.form-group__require 必須
            %br/
            = f.email_field :reservation_email, {autofocus: true, autocomplete: "email", placeholder: "PC・携帯どちらでも可",class:'form-group__input'}
            = f.submit '取り置きする', class: "reserve"
        - if @product.reservation_email.present?
          =link_to reserve_cancel_product_path,method: :patch, class:"btn-default btn-red" do
            取り置きをやめる

機能実装について(コントローラ)

コントローラは以下のように実装してます。
ポイントは以下の通りになってます。
1. reservedアクションで入力された”reservation_email”をproductに追加します。
 *この”reservation_email”が取置き品を購入するためのキーとなります。
2. 取り置きをキャンセルする時と購入がされた後は”reservation_email”のvalueをなくします。

コントローラ(該当箇所のみ)
before_action :set_product, only: [:reserved,:reserve,:reserve_cancel,:purchase]

def reserve
end

def reserved  

#reservedアクションで入力された”reservation_email”をproductに追加します。
 @product.update(product_params)  
 if @product.reservation_email.present?
   else
     render :reserve
  end
end

def reserve_cancel

#”reservation_email”のvalueをなくします。
 if @product.update(reservation_email:"")
  redirect_to product_path
 else
  redirect_to product_path
  end
end

def purchase
  Payjp.api_key = Rails.application.secrets.payjp_access_key
  charge = Payjp::Charge.create(
    amount: @product.price,
    customer: Payjp::Customer.retrieve(@creditcard.customer_id),
    currency: 'jpy'
  )
#”reservation_email”がある場合は、valueをなくします。
  if @product.reservation_email.present?
    @product.update(reservation_email:"")
  end
  @product_buyer= Product.find(params[:id])
  @product_buyer.update( buyer_id: current_user.id)
  redirect_to purchased_product_path
end

private
def product_params
      params.require(:product).permit(:name,:category_id,:price,:explain,:size,:brand_id,:status,:postage,:shipping_date,:prefecture,:reservation_email,images_attributes: [:product_image,:_destroy,:id]).merge(user_id: current_user.id)
end

def set_product
  @product = Product.includes(:comments).find(params[:id])
end

参照

【Rails】updateメソッドの使い方を徹底解説!
https://pikawaka.com/rails/update

【Rails】form_forの使い方を徹底解説!
https://pikawaka.com/rails/form_for#form_for%E3%81%A7%E3%81%AE%E4%BF%9D%E5%AD%98%E6%96%B9%E6%B3%95%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9

Railsのモデルの作成、検索、更新、削除のよく使うメソッドのまとめ
https://ruby-rails.hatenadiary.com/entry/20140724/1406142120

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

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

[取り置き機能]某フリマアプリにオリジナル機能を実装してみた。

某スクールのチーム開発にて某フリマアプリを作成しております。
先日、必須機能の実装が完了し、追加機能を実装していくフェーズになりました。
そこで、自分なりに色々と考えた結果、タイトルに記載いたしました「取り置き機能」を実装しました!
今回は、機能追加を決めるにあたっての背景含めて以下に記載いたします。

今までは既存のコンテンツの実装であったため、様々な記事や情報があったため、自分で実装コンテンツや実装方法を考えるという機会は少し少なかった印象ありました。
しかしながら、今回はオリジナル機能であったため、実際のアプリユーザのことを考えながら実装をしていくのは、非常に楽しかったです!!

より良いコードの書き方や修正すべき点などご意見ございましたら、是非いただけますと幸いです!

背景(「取り置き機能」を追加機能に選んだ理由)

今回、追加機能を選ぶにあたり、ネットで某フリマアプリユーザのコメントをネットで探したり、自分でサイトをいじっていました。
その結果、以下の状況が見えてきたため、今回の「取り置き機能」実装を決断しました。
- 問題・課題:
特定のユーザに販売する際に、出品写真やタイトルなどに”〇〇様向け”などと書くしかできず、他ユーザに購入できないようにするブロックができない。
その状況のため、別ユーザがユーザ名を偽ることで、本来購入をする予定であったユーザが購入できず、別のユーザに購入されてしまうという問題が発生している。

「取り置き機能」実装に関する考え方

今回の機能実装に関して、以下のように考えました。
1. 商品を出品するユーザが、商品に対して”キー”を設定する。
2. その”キー”を持っているユーザは購入をできるが、”キー”を持っていないユーザは購入をできないようにする。

ここで問題となったのは何を"キー”とするかでした。
「出品するユーザが合言葉を設定し、それを入力させる」や「ユーザ名を”キー”とする」などを考えましたが、どれもユニーク性がなかったためボツとし、今回は購入して欲しいユーザのemailアドレスを”キー”とすることにし,”reservation_email”というカラムを追加することにしました。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。

*実際はemailアドレスを"キー”とするのはプライバシーの観点からあまりよろしくはないと思われますため、可能で有れば、会員番号などを”キー”とするのがベターかと考えます。(今回は会員番号を設定していなかったため、アドレスにしました)

実装内容(イメージ)

今回実装した機能のイメージは以下の通りになってます。

出品ユーザ側機能

機能1:取り置きをする

demo

機能2:取り置きをやめる

demo

機能3:その他ユーザに購入できないようにする

demo

購入ユーザ側機能

機能1:取置き品を購入する

demo
*購入すると以下のように、SOLDとなります。
demo
購入についての実装は、以下の記事をご参照ください。
[HowTo]Pay.jpを用いた商品購入機能実装から商品購入後の設定まで
https://qiita.com/Tatsu88/items/eb420e372077939a4627#%E5%95%86%E5%93%81%E8%B3%BC%E5%85%A5%E7%A2%BA%E8%AA%8D%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E7%B7%A8%E9%9B%86

機能実装:マイグレーションファイル

マイグレーションファイルは以下のように実装してます。
今回は、t.string :reservation_emailを追加してます。
こちらが、取り置き機能を実現するための要となります。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。

マイグレーションファイル
class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name, limit: 191,null:false,index: true
      t.integer :price, index: true
      t.text :explain,null:false
      t.integer :postage,null:false
      t.integer :status
      t.integer :shipping_date
      t.integer :size
      t.integer :brand_id
      t.integer :category_id
      t.integer :prefecture
      t.integer :buyer_id
      t.references :user,index: true, foreign_key: true
      t.string :reservation_email
      t.timestamps
    end
  end
end

機能実装:ルーティング

ルーティングは以下のように実装してます。
'reserve'、'reserved',patch 'reserve_cancel'を今回追加してます。
内容としては、予約・予約完了・予約取り消しとなります。

ルーティング
  resources :products do
    member do
      post 'purchase'
      get 'purchased'
      get 'buy'
      get 'reserve'
      patch 'reserved'
      patch 'reserve_cancel'
    end
    resources :comments,only:[:create,:destroy]
  end

機能実装:ビュー

ビューは以下のように実装してます。
基本的に"reservation_email"の情報有無によって表示内容を変えてます。

1. 商品一覧

こちらでは購入済みか取り置き済みかでサムネイルとして表示する写真にラベルを取り付けます。

view
.product__thumbnail--image
 =image_tag (product.images[0].product_image.url)

#購入されていれば、"SOLD"のラベルをつけます。
-if product.buyer_id.present? 
 .items-box_photo__sold
  .items-box_photo__sold__inner SOLD

#取り置きされていれば、"Reserved"のラベルをつけます。
-if product.reservation_email.present? 
 .items-box_photo__reserved
  .items-box_photo__reserved__inner Reserved

2. 商品詳細

こちらではユーザの状態によって、ビューの表示内容を変えてます。

view
.product-buy__btn__box  
#ログインユーザが出品者の場合の、表示内容 
 - if user_signed_in? && current_user.id ==@product.user_id
  = link_to "削除する", product_path(@product.id), method: :delete,class:"product-details-delete__btn"
  = link_to "編集する", edit_product_path(@product.id),class:"product-details-edit__btn"
  = link_to "取り置きする/編集する", reserve_product_path(@product.id),class:"product-details-resorve__btn"

#購入者向けの表示内容
#既に購入されている時
 - elsif @product.buyer_id.present? 
  = link_to "売り切れました",buy_product_path,class:"disabled-button bold"

#取り置きされていて、その取り置きを商品を購入することを許可されているユーザの時
 - elsif @product.reservation_email.present? && @product.reservation_email == current_user.email
  = link_to "取り置き商品を購入する",buy_product_path,class:"product-purchase__btn"

#取り置きされていて、その取り置きを商品を購入することを許可されていないユーザの時
 - elsif @product.reservation_email.present? && @product.reservation_email != current_user.email
  = link_to "取り置き商品のため購入できません",buy_product_path,class:"disabled-button bold"
 - else
  = link_to "購入画面に進む",buy_product_path,class:"product-purchase__btn"

2. 取り置き画面

こちらでは、"reservation_email"というカラムの情報有無によって表示する内容を変えております。
情報が有れば、「取り消す」ボタンが出てきます。

ビュー(取り置き画面)
%main.buy-main 
  .buy-item-container
    %h2.buy-item-head 取り置き内容の確認
    %section.buy-content.buy-item
      .buy-content-inner
        .buy-item-box
          .buy-item-image
            =image_tag(@product.images[0].product_image.url,class:"buy-image")
          .buy-item-detail
            %p.buy-item-name
              =@product.name
              %p.buy-item-price.bold
                = number_to_currency(@product.price,format: "%u%n",unit:"¥",precision: 0)
                %span.item-shipping-fee.f14.bold
                  (税込)送料込み
        =form_for(@product, url: reserved_product_path,method: :patch) do |f|
          .form-group
            =f.label :お取り置きをする方のアドレス
            %span.form-group__require 必須
            %br/
            = f.email_field :reservation_email, {autofocus: true, autocomplete: "email", placeholder: "PC・携帯どちらでも可",class:'form-group__input'}
            = f.submit '取り置きする', class: "reserve"
        - if @product.reservation_email.present?
          =link_to reserve_cancel_product_path,method: :patch, class:"btn-default btn-red" do
            取り置きをやめる

機能実装:コントローラ

コントローラは以下のように実装してます。
ポイントは以下の通りになってます。
1. reservedアクションで入力された”reservation_email”をproductに追加します。
 *この”reservation_email”が取置き品を購入するためのキーとなります。
2. 取り置きをキャンセルする時と購入がされた後は”reservation_email”のvalueをなくします。

コントローラ(該当箇所のみ)
before_action :set_product, only: [:reserved,:reserve,:reserve_cancel,:purchase]

def reserve
end

def reserved  

#reservedアクションで入力された”reservation_email”をproductに追加します。
 @product.update(product_params)  
 if @product.reservation_email.present?
   else
     render :reserve
  end
end

def reserve_cancel

#”reservation_email”のvalueをなくします。
 if @product.update(reservation_email:"")
  redirect_to product_path
 else
  redirect_to product_path
  end
end

def purchase
  Payjp.api_key = Rails.application.secrets.payjp_access_key
  charge = Payjp::Charge.create(
    amount: @product.price,
    customer: Payjp::Customer.retrieve(@creditcard.customer_id),
    currency: 'jpy'
  )
#”reservation_email”がある場合は、valueをなくします。
  if @product.reservation_email.present?
    @product.update(reservation_email:"")
  end
  @product_buyer= Product.find(params[:id])
  @product_buyer.update( buyer_id: current_user.id)
  redirect_to purchased_product_path
end

private
def product_params
      params.require(:product).permit(:name,:category_id,:price,:explain,:size,:brand_id,:status,:postage,:shipping_date,:prefecture,:reservation_email,images_attributes: [:product_image,:_destroy,:id]).merge(user_id: current_user.id)
end

def set_product
  @product = Product.includes(:comments).find(params[:id])
end

参照

【Rails】updateメソッドの使い方を徹底解説!
https://pikawaka.com/rails/update

【Rails】form_forの使い方を徹底解説!
https://pikawaka.com/rails/form_for#form_for%E3%81%A7%E3%81%AE%E4%BF%9D%E5%AD%98%E6%96%B9%E6%B3%95%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9

Railsのモデルの作成、検索、更新、削除のよく使うメソッドのまとめ
https://ruby-rails.hatenadiary.com/entry/20140724/1406142120

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

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

rails:ページ移行で、(navbarなど)リンク表示を変更する

いらないリンクを消したい時

ログインページにいるのにログインリンク。新規登録ページにいるのにsignup(新規登録)のリンクはいらないですよね。
僕も今回、学習段階で実装する場面が来たので簡単にご紹介します。

・新規登録ページ

navbarのリンクはlogin表示のみ。signup(新規登録)リンクは隠す
スクリーンショット 2020-02-22 19.25.25.png

・ログインページ

navbarのリンクはsignup(新規登録)表示のみ。loginリンクは隠す
スクリーンショット 2020-02-22 19.04.45.png

該当コード

request:ユーザのヘッダー情報や環境変数を取得

#新規登録画面におけるnavbarのコード

<% unless request.path.include?("login") %> #"login"とのurlを含まなければtrue
    <li class="nav-item"><%= link_to 'Log in', login_path, class:'nav-link' %></li>
<% end %>


#ログイン画面におけるnavbarのコード

<% unless request.path.include?("users/new") %> #"users/new"とのurlを含まなければtrue
    <li class="nav-item"><%= link_to 'Sign up', new_user_path, class:'nav-link'%></li>
<% end %>
#"signup"または"users/new"とのurlを含まなければtrue

<% unless request.path.include?("signup") || request.path.include?("users/new")%>
    <li class="nav-item"><%= link_to 'Sign up', new_user_path, class:'nav-link'%></li>
<% end %>

補足

足りない部分や、間違っている箇所、もっときれいにコードを書ける部分があればご指摘いただきたいです。

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

ネストしたフォームを簡潔に実装できるcocoon gemをwebpack環境でセットアップする

前置き

cocoonは、ネストしたフォームをサポートするgem

Railsガイドにも説明のあるネストしたフォーム。この実装をサポートしてくれるgemです。

「レシピ」と「手順」が1:Nの関連をもつときに、レシピの登録のとき手順も同時に(複数)作成するときなどに便利です。

レシピと手順の関連の例
class Recipe < ApplicationRecord
  has_many :steps
end

class Step < ApplicationRecord
  belongs_to :recipe
end

このようなフォームをつくれます。

cocoon_form.gif

環境

  • Ruby (2.7.0)
  • Rails (6.0.2.1)
  • cocoon (1.2.14)
Gemfile
gem 'cocoon'

Rails6環境で、cocoon gemをつかうための前提知識

以下の点を覚えておく必要があります。

本題:webpackでcocoonを組み込む方法

1. jQueryをセットアップする

2. cocoon をセットアップする

以下のスレッドを参考にしました。

Gemfileに追加し、bundle installします。

Gemfile
gem 'cocoon'

jQueryとおなじように、yarn addで追加していきます。このとき、対象のライブラリ名の指定が特殊になります。(cocoonではなくてgithub:nathanvda/cocoon#c24ba53になります。)

yarn add github:nathanvda/cocoon#c24ba53

実行後に、app/assets/javascripts/cocoon.jsにファイルができているはずです。package.jsonに以下の記述が追加されていればOK。

package.json(追加された行)
"cocoon": "github:nathanvda/cocoon#c24ba53"

最後に application.js にimportを追加してください。

app/javascript/packs/application.js
import "cocoon";

よくわかっていないところ

コミット番号については、参照先のスレッドにも

コミット番号 #c24ba53 は重要な意味があるので省略してはいけない。

という説明だけでした。このコミットでwebpackに対応した変更が入ったのだとおもわれます。(詳細未調査)

おわりに

cocoonの公式レポジトリにあるように、Rails6での使用法は明記されていません(正式対応はもうすこし先?)、ですのでここで説明した方法は今後のアップデートで変更になる場合がありますのでご了承ください。

フロントエンド側のライブラリ(cocoon, jQuery, bootstrapなど)はwebpackに管理させる方法にまだ慣れていなくて、前の方法で動作しない!と焦って調べ始めたことが今回の記事のきっかけでした。 :sweat_smile:

おなじような状況にあったときに、もしかしてwebpackかも?と考える選択肢が増えると解決の糸口がみつかるかもしれませんね!

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

ユーザ情報をどうDB設計するか突き詰めて考えてみる

この記事について

  • システム開発するうえで絶対に避けては通れないユーザ情報周りの設計を自分なりに考察してみた
  • 結局何が最適かわからないけど色々と考え出すと奥が深かった

きっかけ的な

Railsでアプリ作ったんだけど、bootstrapとかdeviseとか使って横着したので、正直やった感ないなーって思いました。HTML/CSS多少勉強しなおして、あとこの本読んで、もう一回横着しないでなんちゃってQiitaみたいななんちゃってメディアシステムみたいなの作ろうかなと。そうなると、当然ユーザ情報の設計とか考えなきゃなんですが、いろいろ考えたら奥が深かったのでQiitaに載せて整理しようかなーって思いました。あと、ここに公開すればありがたいお言葉を頂戴いただけるのではないかとか思ったり。

本記事のユーザ情報の定義

利用用途

  • ログイン認証に用いる。
  • サインアップ画面より、新規登録をユーザ自身で可能とする。
  • ユーザによるプロフィール編集を可能とする。
  • プロフィールは他ユーザにより、CMS上のある画面から一覧表示できる。また、個別のユーザはユーザ名で検索可能とする。
  • プロフィール詳細はほかユーザにより、CMS上のある画面から確認可能とする。
  • ユーザ削除時は、ログイン情報も含めてすべて物理削除する

項目定義

  • 一意のログインID、パスワードをもつ。また、最終ログイン日時を保持する。
  • CMS内で利用される一意のユーザ名を保持する。
  • 上記ユーザ名とは別でユーザの氏名、氏名カナを保持する。ユーザの氏名、氏名カナは他ユーザは見れないものとする。
  • ユーザ登録時や運営お知らせに利用するためのメールアドレスを保持する。
  • プロフィール情報として年齢、誕生日、自由記述欄を保持する。

私の設計

こうなりました。詳細は追ってですが、この後簡単に思考プロセスを書いていきます。なお、簡単にするため、インデックスどうするとかは一旦考えません。
image.png

テーブルにどう持たせるか

まあ単純に考えるとusersテーブルにえいやって全項目突っ込みたくなりますよね。こんな感じです。
image.png
別に個人で勉強用途に使う程度ならこれでいいんですが、ある程度ユーザ増えるという前提で考えると、ユーザに関する全操作のたびにテーブルがロックされて、パフォーマンス落ちるんじゃない?とかデータ量多くなって適切にインデックス設計とかしても限界ありそうだよねとか思ったわけです。あと、このテーブルにカラム追加するとか、検索のためのタグ付けするとか、そういうの考えるとなんだか背筋が凍ります。特に住所情報持たせるとかなったらとんでもないことになりそう。

テーブルを分けてみる

要件に立ち返ると、認証、プロフィールとあるので、この観点で分けてみます。
image.png
ログインIDとパスワード、最終ログイン日時をauthenticatesとして定義しました。なお両テーブルは1:1で関連付けられています。
とりあえずusersテーブルのごった煮感はなくなりましたが、profileもう少しなんとかできないかなーとか思うので、もう少し分けてみます。
よく見るとほかユーザに見られてほしくない氏名の情報が混じってるのでこれは分けたほうがよさげ。と思ったのですが、、、
image.png
いい感じに分けられたんですけど、なんかセンスないというか、どう関連付けていいのかぱっと見わからないですね。。。
というわけで、fullnamesを親にして、profilesとauthenticatesと関連付けてみます。
image.png
なんかいい感じに見えました。でも親にfullnameってのがなんかあれですね。もう分けてしまいましょう。
image.png
いい感じになったけど、なんかこれでいいのか感があります。ちょっと大げさに分けすぎてしまったかな。。。
なんかもやっとしてますけど、これで終わりにします。名前のセンスないなとかそういうのはありますが。。。

最後に

もやっとした感じで終わってしまったので、なんか不完全燃焼ですが、これはこれで結論ということで。
authenticatesとusersの関連付けっているのかな。正直model側でdestroyの依存をかけてしまうだけでいい気もする。でも、直接SQLで触られたりすると一気にデータがおかしくなる。。。
あと、Railsの規約って観点でもどうなのかなとも少し思ったりもした。

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

ユーザ情報のDB設計について突き詰めて考えてみる

この記事について

  • システム開発するうえで絶対に避けては通れないユーザ情報周りの設計を自分なりに考察してみた
  • 結局何が最適かわからないけど色々と考え出すと奥が深かった

きっかけ的な

Railsでアプリ作ったんだけど、bootstrapとかdeviseとか使って横着したので、正直やった感ないなーって思いました。HTML/CSS多少勉強しなおして、あとこの本読んで、もう一回横着しないでなんちゃってQiitaみたいななんちゃってメディアシステムみたいなの作ろうかなと。そうなると、当然ユーザ情報の設計とか考えなきゃなんですが、いろいろ考えたら奥が深かったのでQiitaに載せて整理しようかなーって思いました。あと、ここに公開すればありがたいお言葉を頂戴いただけるのではないかとか思ったり。

本記事のユーザ情報の定義

利用用途

  • ログイン認証に用いる。
  • サインアップ画面より、新規登録をユーザ自身で可能とする。
  • ユーザによるプロフィール編集を可能とする。
  • プロフィールは他ユーザにより、CMS上のある画面から一覧表示できる。また、個別のユーザはユーザ名で検索可能とする。
  • プロフィール詳細はほかユーザにより、CMS上のある画面から確認可能とする。
  • ユーザ削除時は、ログイン情報も含めてすべて物理削除する

項目定義

  • 一意のログインID、パスワードをもつ。また、最終ログイン日時を保持する。
  • CMS内で利用される一意のユーザ名を保持する。
  • 上記ユーザ名とは別でユーザの氏名、氏名カナを保持する。ユーザの氏名、氏名カナは他ユーザは見れないものとする。
  • ユーザ登録時や運営お知らせに利用するためのメールアドレスを保持する。
  • プロフィール情報として年齢、誕生日、自由記述欄を保持する。

私の設計

こうなりました。詳細は追ってですが、この後簡単に思考プロセスを書いていきます。なお、簡単にするため、インデックスどうするとかは一旦考えません。
image.png

テーブルにどう持たせるか

まあ単純に考えるとusersテーブルにえいやって全項目突っ込みたくなりますよね。こんな感じです。
image.png
別に個人で勉強用途に使う程度ならこれでいいんですが、ある程度ユーザ増えるという前提で考えると、ユーザに関する全操作のたびにテーブルがロックされて、パフォーマンス落ちるんじゃない?とかデータ量多くなって適切にインデックス設計とかしても限界ありそうだよねとか思ったわけです。あと、このテーブルにカラム追加するとか、検索のためのタグ付けするとか、そういうの考えるとなんだか背筋が凍ります。特に住所情報持たせるとかなったらとんでもないことになりそう。

テーブルを分けてみる

要件に立ち返ると、認証、プロフィールとあるので、この観点で分けてみます。
image.png
ログインIDとパスワード、最終ログイン日時をauthenticatesとして定義しました。なお両テーブルは1:1で関連付けられています。
とりあえずusersテーブルのごった煮感はなくなりましたが、profileもう少しなんとかできないかなーとか思うので、もう少し分けてみます。
よく見るとほかユーザに見られてほしくない氏名の情報が混じってるのでこれは分けたほうがよさげ。と思ったのですが、、、
image.png
いい感じに分けられたんですけど、なんかセンスないというか、どう関連付けていいのかぱっと見わからないですね。。。
というわけで、fullnamesを親にして、profilesとauthenticatesと関連付けてみます。
image.png
なんかいい感じに見えました。でも親にfullnameってのがなんかあれですね。もう分けてしまいましょう。
image.png
いい感じになったけど、なんかこれでいいのか感があります。ちょっと大げさに分けすぎてしまったかな。。。
なんかもやっとしてますけど、これで終わりにします。名前のセンスないなとかそういうのはありますが。。。

最後に

もやっとした感じで終わってしまったので、なんか不完全燃焼ですが、これはこれで結論ということで。
authenticatesとusersの関連付けっているのかな。正直model側でdestroyの依存をかけてしまうだけでいい気もする。でも、直接SQLで触られたりすると一気にデータがおかしくなる。。。
あと、Railsの規約って観点でもどうなのかなとも少し思ったりもした。

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

Railsチュートリアル bitbucketにプッシュできない

bitbucketにプッシュする方法。
軽くハマったので備忘録。

git push -u origin --all

だと失敗するので、

git push --mirror git@bitbucket.org:[ユーザー名]/sample_app

こっちを使いましょう。

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

[Ruby on Rails]画像投稿ボタンをFont Awesomeのアイコンにする方法

概要

表題の通りですが、調べるとHaml記法で書かれた投稿しか見つけられず、今のところHaml記法を使っていない私にはわかりづらかったので、メモがてら共有させていただきます。

環境

Ruby:2.6.3
Rails:5.1.6
bootstrap:4.4.1
FontAwesome

方法

1.file_fieldで画像投稿ボタンを表示。
2.file_fieldボタンによって出力されるinputタグを見えなくする(dislay:none)。
3.file_fieldの上にFont Awesomeのアイコンをiタグで表示。
4.iタグをlabelタグで囲って、アイコンをボタンとして有効にする。

home.html.erb
<%= form_for(@dreampost) do |f| %>
  <div class="field">
    <%= f.text_area :content, placeholder: "投稿できます" %>
  </div>
  <div class="space-between">
    <span class="picture">
     <label for="dreampost_picture">
       <i class="far fa-image"></i>
      </label>    
      <%= f.file_field :picture, placeholder: '&#xf0a8',accept: 'image/jpeg,image/gif,image/png' %>
      <div class="clear"></div> 
    </span>
  <%= f.submit "送信", class: "btn btn-primary" %>
<% end %>
custom.scss
.space-between { display: flex; justify-content: space-between; }

.picture>input { display: none; }

.picture>label { margin-bottom: 0; float: left; }

.fa-image { color: #fff; float: left; }

.fa-image::before { font-size: 2rem; }

.clear { clear: both; }

スクリーンショット 2020-02-22 10.08.56.png

補足

div.classに{ display: flex; justify-content: space-between;}と、
labelタグとiタグにfloat:leftで、
アイコンを左寄せ、送信ボタンを右寄せにしています。

ご指摘などございましたら、ぜひよろしくお願いいたします。

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

Rails でトークン認証 API を 15 分で実装する

下記を作成してみます。SPA のバックエンド側を想定しています。

  • /auth でログインし token を発行してもらえる。
  • /users でユーザー一覧を取得できる。
    • ただし token が必要。
    • 更に admin ユーザーのみでき、member ユーザーはアクセス許可がない。

上記を利用したフロント側の記事も書いておりますので宜しければご覧ください。
Vue.js で簡単なログイン画面 (トークン認証) を作ってみた

User API 作成

rails new で API モードで新規アプリ作成します。

$ rails new yourappname --api

scaffold で User Model と Controller を作ります。
lock_version というカラムを追加すると Rails で楽観ロックを実装してくれます。便利ですね。

$ cd yourappname
$ rails g scaffold User name:string \
email:string \
role:integer \
password_digest:string \
register_user:integer \
update_user:integer \
lock_version:integer \
activated_at:datetime \
deleted_at:datetime

seeds に初期データを書いてみましょう。

db/seeds.rb
User.create!([
  {
    name: 'admin',
    email: 'admin@example.com',
    role: 'admin',
    password_digest: '$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK',
    register_user: 1,
    update_user: 1,
    lock_version: 0,
    activated_at: '2020-02-03 00:00:00',
    deleted_at: nil,
  },
  {
    name: 'member',
    email: 'member@example.com',
    role: 'member',
    password_digest: '$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK',
    register_user: 1,
    update_user: 1,
    lock_version: 0,
    activated_at: '2020-02-03 00:00:00',
    deleted_at: nil,
  },
])

開発用の DB をマイグレーション & 初期データを投入します。

$ rails db:migrate && rails db:seed

サーバーを起動します。

$ rails s

別ターミナルにて curl で叩くと初期データが返ってきます。
まずは簡単な API ができたことを確認します。:smile:

# 別ターミナル
$ curl -s http://localhost:3000/users | jq
[
  {
    "id": 1,
    "name": "admin",
    "email": "admin@example.com",
    "role": "admin",
    "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK",
    "register_user": 1,
    "update_user": 1,
    "lock_version": 0,
    "activated_at": "2020-02-03T00:00:00.000Z",
    "deleted_at": null,
    "created_at": "2020-02-22T04:08:58.769Z",
    "updated_at": "2020-02-22T04:08:58.769Z"
  },
  {
    "id": 2,
    "name": "member",
    "email": "member@example.com",
    "role": "member",
    "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK",
    "register_user": 1,
    "update_user": 1,
    "lock_version": 0,
    "activated_at": "2020-02-03T00:00:00.000Z",
    "deleted_at": null,
    "created_at": "2020-02-22T04:08:58.775Z",
    "updated_at": "2020-02-22T04:08:58.775Z"
  }
]

テストも実行してエラーが無いことも確認しましょう。

$ rails t
Running via Spring preloader in process 16858
Run options: --seed 26013

# Running:

.....

Finished in 0.320389s, 15.6060 runs/s, 21.8484 assertions/s.
5 runs, 7 assertions, 0 failures, 0 errors, 0 skips

ちなみに jq が入っていない場合は入れると JSON 整形してくれて便利です。

$ brew install jq

Gemfile もろもろ

Gemfile は Node.js で言う、package.json 的なファイルだと思います。

下記を追記します。ついでにオススメの gem も。

gem 'redis-rails' # Redis を扱うための gem
gem 'mock_redis' # Redis のモック。テスト実行時に使用。
gem 'config' # 環境ごとに yml の設定ファイルを作成可能。
gem 'pundit' # 認証周りを REST ベースでシンプルに実装できる。
gem 'paranoia' # 論理削除できる。

下記をコメントアウトします。
has_secure_password を使う際に必要です。
これはテーブルに password_digest というカラムを用意すると、Rails がパスワードをハッシュ化してくれます。

gem 'bcrypt', '~> 3.1.7'

bundle install するとパッケージがインストールされます。

$ bundle install

config をインストールすると rails g config:install が使えます。
環境ごとに yml ファイルができます。便利ですね。

$ rails g config:install
Running via Spring preloader in process 17408
      create  config/initializers/config.rb
      create  config/settings.yml
      create  config/settings.local.yml
      create  config/settings
      create  config/settings/development.yml
      create  config/settings/production.yml
      create  config/settings/test.yml
      append  .gitignore

Redis 設定

セッション情報を保存するためにインメモリ DB の Redis を使います。
Mac の方は brew でインストール。

$ brew install redis

redis-server で起動できます。簡単でいいですね。

$ redis-server

いったん、全環境共通の settings.yml に url と timeout を追記します。
こうすると例えば、下記の url の値を取り出すには Settings.session.url と記述すれば OK です。

config/settings.yml
session:
  url: redis://localhost:6379
  timeout: 7200
role:
  member: member
  admin: admin

ところで Redis 接続時に毎回 Redis.new() を書くのは不便なので、initializers/ 下に redis.rb を作成します。
ついでにテストの時は Redis 立ち上げなくても済むようにしました。

config/initializers/redis.rb
if Rails.env.test?
  REDIS = MockRedis.new
else
  REDIS = Redis.new(url: Settings.session.url)
end

Auth コントローラー作成

コマンドラインから枠を作ります。

$ rails g controller auth

auth のルーティングを追加します。
個人的にはなるべく resources を使うようにすると綺麗だと思います。

config/routes.rb
Rails.application.routes.draw do
  resources :users
+  resources :auth, :only => [:create, :destroy]
end

セッション作成処理を concerns に切り出してみます。

app/controllers/concerns/session.rb
module Session
  def self.create(user)
    token = SecureRandom.hex(64)
    REDIS.mapped_hmset(
      token,
      'user_id' => user.id,
      'role' => user.role,
    )
    REDIS.expire(token, Settings.session.timeout)
    return token
  end
end

AuthController で先ほど作った Session モジュールを利用します。

app/controllers/auth_controller.rb
class AuthController < ApplicationController
+  def create
+    user = User.find_by(email: params[:email])
+    token = ''
+    status = :unauthorized
+    if user && user.authenticate(params[:password])
+      token = Session.create(user)
+      status = :created
+    end
+    render json: { token: token }, status: status
+  end
end

models/user.rb に下記を追記。
物理削除とパスワードハッシュ化を利用します。
enum で member と admin も定義しておきます。

app/models/user.rb
class User < ApplicationRecord
+  acts_as_paranoid
+  has_secure_password
+  enum role: { member: 0, admin: 1 }
end

動作確認してみましょう。

ログイン OK

まずは seeds.rb に記述した email と password で /auth を叩きます。
token が返ってきますね:smile:

$ curl -s \
-X POST http://localhost:3000/auth \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "password1234"}' 

{"token":"ed64bbcf830e4feca9203b0955c5916fda815a23da80da8a7bb62b8c6466dbc34a07e21e9dd96447882fc8b9cb06121c8ee414b79ffedef4892e7197bbe9edd1"}

redis-cli で Redis に接続して、トークンが作成されたか確認してみましょう。
おお、できています!:joy:

$ redis-cli
127.0.0.1:6379> keys *
1) "ed64bbcf830e4feca9203b0955c5916fda815a23da80da8a7bb62b8c6466dbc34a07e21e9dd96447882fc8b9cb06121c8ee414b79ffedef4892e7197bbe9edd1"

ログイン NG

password を適当に変えてみます。
token が空になっていますね。

$ curl -s -X POST http://localhost:3000/auth \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "wrongpassword"}'

{"token":""}

認証を設定

Session モジュールにセッション情報を取得する function を追加します。

app/controllers/concerns/session.rb
module Session

+  def self.get(token)
+    REDIS.hgetall(token)
+  end

  def self.create(user)
    token = SecureRandom.hex(64)
    REDIS.mapped_hmset(
      token,
      'user_id' => user.id,
      'role' => user.role,
    )
    REDIS.expire(token, Settings.session.timeout)
    return token
  end
end

続いて基幹コントローラーに手を入れます。

  • authenticate_with_http_token を利用すると、 リクエストヘッダに 'Authorization: Token hogehoge' がセットされていた場合に、トークン hogehoge を取り出せます。
    • 上記を利用するには include ActionController::HttpAuthentication::Token::ControllerMethods する必要があります。
  • before_action :set_session で、Redis に登録してあるセッション (ここでは user.id と user.role ) をメンバにセットしています。
  • before_action :require_login で、基本的にセッションがない場合に認証エラーとしています。
app/controllers/application_controller.rb
class ApplicationController < ActionController::API
+  include ActionController::HttpAuthentication::Token::ControllerMethods
+
+  before_action :set_session
+  before_action :require_login
+
+  @session = {}
+
+  def require_login
+    render json: { error: 'unauthorized' }, status: :unauthorized if @session.empty?
+  end
+
+  private
+    def set_session
+      authenticate_with_http_token do |token, options|
+        @session = Session.get(token)
+      end
+    end
end

AuthController に skip_before_action を追加します。
これを追加しないと一生ログインができません・・。

app/controllers/auth_controller.rb
class AuthController < ApplicationController
+  skip_before_action :require_login, only: [:create]
+
  def create
    user = User.find_by(email: params[:email])
    token = ''
    status = :unauthorized
    if user && user.authenticate(params[:password])
      token = Session.create(user)
      status = :created
    end
    render json: { token: token }, status: status
  end
end

ユーザー一覧 OK

Authorization: Token xxx には、先ほど /auth を叩いて得られた token をセットします。
/users の結果が返ってきています。

$ curl -s http://localhost:3000/users \
-H "Content-Type: application/json" \
-H "Authorization: Token ed64bbcf830e4feca9203b0955c5916fda815a23da80da8a7bb62b8c6466dbc34a07e21e9dd96447882fc8b9cb06121c8ee414b79ffedef4892e7197bbe9edd1" \
| jq

[
  {
    "id": 1,
    "name": "admin",
    "email": "admin@example.com",
    "role": 0,
    "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK",
    "register_user": 1,
    "update_user": 1,
    "lock_version": 0,
    "activated_at": "2020-02-03T00:00:00.000Z",
    "deleted_at": null,
    "created_at": "2020-02-21T14:28:10.627Z",
    "updated_at": "2020-02-21T14:28:10.627Z"
  },
  {
    "id": 2,
    "name": "member",
    "email": "member@example.com",
    "role": 0,
    "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK",
    "register_user": 1,
    "update_user": 1,
    "lock_version": 0,
    "activated_at": "2020-02-03T00:00:00.000Z",
    "deleted_at": null,
    "created_at": "2020-02-21T14:28:10.635Z",
    "updated_at": "2020-02-21T14:28:10.635Z"
  }
]

ユーザー一覧 NG

token を適当なものに変えるとちゃんと認証エラーになりました:relaxed:

$ curl -s http://localhost:3000/users -H "Content-Type: application/json" -H "Authorization: Token wrong_token" | jq

{
  "error": "unauthorized"
}

権限周り設定

Web アプリでは権限は必須と言えます。
Pundit を使うと Rest ベースでシンプルに実装できます。

$ rails g pundit:install
$ rails g pundit:policy user

まずは application_policy.rb に管理者かどうかの admin? を追加してみます。

app/policies/application_policy.rb
class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    false
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

+  def admin?
+    @user['role'] == Settings.role.admin
+  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.all
    end
  end
end

続いて、user_policy.rb の index に先ほど作った admin? を追加します。

app/policies/user_policy.rb
class UserPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      scope.all
    end
  end

+  def index?
+    admin?
+  end
end

あとは利用側です。
application_controller.rb で Pundit を include し、current_user メソッドを追加します。
また、Pundit の NotAuthorizedError を拾えるように rescue_from を追加します。
(エラー処理は増えてきたら concerns に切り出すと良いと思います。)

app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods
+  include Pundit

  before_action :set_session
  before_action :require_login

  @session = {}

+  rescue_from Pundit::NotAuthorizedError do |e|
+    render json: { detail: e.message }, status: :unauthorized
+  end
+
  def require_login
    render json: { error: 'unauthorized' }, status: :unauthorized if @session.empty?
  end

+  def current_user
+    @session
+  end

  private
    def set_session
      authenticate_with_http_token do |token, options|
        @session = Session.get(token)
      end
    end
end

後は users_controller.rb の index に Pundit を追加します。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :update, :destroy]

  # GET /users
  def index
-    @users = User.all
-
-    render json: @users
+    users = authorize Pundit.policy_scope(@session, User)
+    render json: users
  end

  # GET /users/1
  def show
    render json: @user
  end

  # POST /users
  def create
    @user = User.new(user_params)

    if @user.save
      render json: @user, status: :created, location: @user
    else
      render json: @user.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /users/1
  def update
    if @user.update(user_params)
      render json: @user
    else
      render json: @user.errors, status: :unprocessable_entity
    end
  end

  # DELETE /users/1
  def destroy
    @user.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a trusted parameter "white list" through.
    def user_params
      params.require(:user).permit(:name, :email, :role, :password_digest, :register_user, :update_user, :lock_version, :activated_at, :deleted_at)
    end
end

さて、動作確認してみましょう。
まずは member ユーザーでログインし、/users を叩きます。
ユーザー一覧が取得できないことを確認します。

$ curl -s -X POST http://localhost:3000/auth -H "Content-Type: application/json" -d '{"email": "member@example.com", "password": "password1234"}'
{"token":"482125111d11c2882cad25b700221a45bc64d8745771f427cb8039337c0e7cda95fc8498f98d6d784781b177afe84699bccb2d62628b1bee3ccee0e96eb9e576"}

$ curl -s http://localhost:3000/users -H "Content-Type: application/json" -H "Authorization: Token 482125111d11c2882cad25b700221a45bc64d8745771f427cb8039337c0e7cda95fc8498f98d6d784781b177afe84699bccb2d62628b1bee3ccee0e96eb9e576"
{"error":"not allowed to index? this User::ActiveRecord_Relation"}

続いて admin ユーザーでログインし、/users を叩きます。
結果が取得できましたね:smile:

$ curl -s -X POST http://localhost:3000/auth -H "Content-Type: application/json" -d '{"email": "admin@example.com", "password": "password1234"}'
{"token":"a87dee3d5cb1592e3b3b09f78931e26254292f1453ea77a1d491fd949ce508d5ea7e00dec6163fb83ed9c7f5d39b672aa429c770a6d0487952022fc55a493487"}

$ curl -s http://localhost:3000/users -H "Content-Type: application/json" -H "Authorization: Token a87dee3d5cb1592e3b3b09f78931e26254292f1453ea77a1d491fd949ce508d5ea7e00dec6163fb83ed9c7f5d39b672aa429c770a6d0487952022fc55a493487"
[{"id":1,"name":"admin","email":"admin@example.com","role":"admin","password_digest":"$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK","register_user":1,"update_user":1,"lock_version":0,"activated_at":"2020-02-03T00:00:00.000Z","deleted_at":null,"created_at":"2020-02-11T12:14:52.557Z","updated_at":"2020-02-11T12:14:52.557Z"},{"id":2,"name":"member","email":"member@example.com","role":"member","password_digest":"$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK","register_user":1,"update_user":1,"lock_version":0,"activated_at":"2020-02-03T00:00:00.000Z","deleted_at":null,"created_at":"2020-02-11T12:14:52.565Z","updated_at":"2020-02-11T12:14:52.565Z"}]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像を希望ページへリンクさせる

画像を希望ページへリンクさせてクリックしたら遷移させる

上記の機能を実装する際、手こずったので備忘録として残しておきます。

aタグでリンクさせようと思い、書き方を調べて相対パスで記述する事にした。
aタグ パス指定
下記のコードで実行したが、Routing Errorが起きた。

tops/index.html.erb
<a href="groups/index.html.erb"><img src="assets/1.png" class="img-fluid" alt="Responsive image"></a>

image.png

下記の方法も試したが、同様にRouting Eroorが起きた。

| ◆ リンク先などの記述例 |
同じフォルダ内にリンクするときは、ファイル名のみ(または ./ に続けてファイル名)を記述します。
下位階層にリンクするときは、フォルダ名に続けて / を入れ、ファイル名を記述します。
上位階層にリンクするときは、../ に続けてファイル名を記述します。2つ上の階層を指定するときは、../../ に続けてファイル名を記述します。
並列した階層のフォルダにリンクするときは、../ に続けてフォルダ名と / を入れ、ファイル名を記述します。

解決法

link_toを使用して、記述することにした。
まずは、ルーティングの設定

routes.rb
    Rails.application.routes.draw do
      devise_for :users
      root 'tops#index'
      resources :groups, only: :index
    end

次に、コントローラー

groups_controller.rb
  class GroupsController < ApplicationController
    def index
    end
  end

最後にビュー

tops/index.html.erb
    <%= link_to (groups_path) do%>
      <img src="assets/1.png" class="img-fluid" alt="Responsive image">
    <% end%>

上記手順で画像リンクに成功した。

なぜaタグでできなかったのか?

・railsでリンクさせる場合は相対パスが使用できない?
・rails,rubyのバージョンが相対パスの仕様に対応してない?
(rails 5.0.7.2 , ruby 2.5.1)

確証が持てないのでわかる方はぜひコメントお願いします!!!!!

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

【Rails+Redis】セッション管理をredisに任せる

現在作成してるrailsポートフォリオで、せっかくだからセッション管理はredisに任せようと思い、色々いじったので共有したいと思います。

環境

  • macOS
  • Docker
  • RubyonRails
  • Redis
  • MySQL

前提

  • Redisはすでにインストール済み
  • DockerにもRedisは追加済み

セッション管理をRedisに任せる理由

RedisはRDBとは違って、

  • KVS
  • データをメモリに保つので動作が高速

という特徴があります。

この特徴からセッション管理に向いてるとして結構使われることが多いみたいです。

さっそく実装してみる

ではさっそく実装しましょう。

gemの追加

Gemfileに以下を追加してインストールしてください。

# Use Redis adapter to run Action Cable in production
gem 'redis', '~> 4.0'
gem 'redis-rails'

config/initializers/session_store.rbの設定

初期設定はコメントアウトしておきましょう。
最初からない人は問題ないです。

config/initializers/session_store.rb
#Rails.application.config.session_store :cookie_store, key: 'XXXXXX'

config/environments/development.rbの設定

config/environments/development.rbに以下を追加。

config/environments/development.rb
config.session_store :redis_store, servers: 'redis://redis:6379/0', expire_in: 1.day

これで一日セッションが保たれる設定になってるはずです。

確認

以下のコマンドでデータが入ってるか確認しましょう。

docker exec -it コンテナID redis-cli
127.0.0.1:6379> keys *
1) "2::63c3d325054ad936b6408b30ba8c4e1395cce110d77c3e5171c7e22840d75a9d"
2) "2::383c6dfd3c95d07631fc8e879911fef09760a789461d8b7d0736a32d7d8ee656"

keys * で出てきたkeyを使って中身を確認。

127.0.0.1:6379> get "2::63c3d325054ad936b6408b30ba8c4e1395cce110d77c3e5171c7e22840d75a9
d"

何か長い謎の文字列が出てきたら情報がしっかり入ってることになっていると思います。

試しにredisのデータを全部削除したら、アプリがログイン画面に戻りましたので、成功と見て問題ないでしょう。

まとめ

以前Redisを利用した時はランキング機能の実装だったので、また違った角度でRedisを扱えたことは勉強になりました。

ぶっちゃけ、まだRedisの知識がちゃんと身に付いてるとは言い難いですが、これからも色々利用しながら学んでいきたいと思います。

ではでは。

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

Ruby on Rails チュートリアル学習記録 第1章

 はじめに

初投稿なので簡単に自己紹介。
私は独学でプログラミング学習を始めたアラサーの男性です。
未経験からエンジニアへの転職を目指しています。
ProgateでHTML/CSS/Ruby/Ruby on Rails5をひと通り学習後Railsチュートリアル開始。
Progateと違い詰まったら自分で調べたり考えたりしながら進めていく必要がありそうなので学習記録を付けることにしました。
主に自分が詰まったところについて書いていくつもりなので章ごとに文章量は疎らになっていくと思います。

1.1

特になし

1.2

特になし

1.3

gemfileの内容を書き換えbundle installを実行するとエラー発生

You have requested:
  spring = 2.0.2

The bundle currently has spring locked at 2.1.0.
Try running `bundle update spring`

If you are updating multiple gems in your Gemfile at once,
try passing them all to `bundle update`

bundle updateを実行後、bundle installを再度実行で解決。

1.4

Rails チュートリアルにおいては、マニュアル作成時点ではGithubで非公開型レポジトリを無料で使えなかったためにBitbucketを使用していようだが、現在はGitHubでも非公開型レポジトリを無料で使えるようになっていたので、自分は既にアカウントを作っていたGitHubを使用。
GitHubでもチュートリアル通りに進めて特に問題は発生せず。

1.5

特になし

1.6

特になし

感想

マニュアル通りに進めていれば特に困ることはなかった。
正直やってることの意味はきちんと理解できていない部分もあると思うが、どうせ2週やるつもりなのでそこはスピード感重視で。
ファイルの内容を変更した後、保存をし忘れてうまくいかないというケアレスミスが何度かあったので気を付けたい。

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

【これで無理なら諦めろ】オブジェクト指向を世界一分かりやすく説明する

はじめに

個人的にオブジェクト指向を理解するのに苦戦しまくったので、その真髄を世界一分かりやすく説明していきます。
何か間違いなどあればどんどんご教示お願いします。言語はRubyです。

対象読者

  • Ruby、Railsを勉強中の方
  • 業務でRubyを使っている方
  • チェリー本やRailsチュートリアルで挫折しちゃった人

オブジェクト指向とは

オブジェクト指向とは、プログラムを「手順」ではなく、「モノの作成と反応」として見る考え方です。これだけではイマイチ分からないと思うので、具体的なコードを使って説明していきます。

オベジェクト指向を理解するには、手続き型のコードを知るのが早いです。

例えば、ある数字を加工して、その数字を使って文章を作って、結果を出力するとき、手続き型では以下のように書きます。

def calculate(number_1, number_2)
  number_1 * number_2 + 1
end

def update_content(value)
  "結果は#{value}です"
end

def output(content)
  puts number
end

value = 100
content = ""

new_value = calculate(value,20)
// 2001

content = update_content(new_value)
// "結果は2001です"

output(content)
=> "結果は2001です"

一方で、オブジェクト指向で書くとこんな感じ。

num = Num.new(100,"")
num.calculate(20)
// 2001

num.update_content
// "結果は2001です"

num.output
=> "結果は2001です"

なんとなく違いは分かりますか?
前者は、先に関数を定義して、処理を実行するたびにいろんなメソッドに値を飛ばして、返ってきた値を変数に代入して...
と流れるようにプログラムが進んでいきます。

なんだか忙しそうですよね?

一方で後者は、はじめにnumというモノを作成し、そいつに対して「計算して」「中身を更新して」「出力して」と呼びかけるような感じで処理が進んでいきます。

情報の記憶も処理の内容も「モノ」が全部やってくれるので、一度作ってしまえば見通しよく実行できるわけです。

この後者こそがまさに、オブジェクト指向の考え方であり、今回でいう所のnumオブジェクトと呼ばれるものです。

つまり、オブジェクト指向とは、コードにオブジェクトという主人公を登場させ、それをメインに話を進めていく設計の考え方といえます。

やっぱり、ドラマを見るにも、主人公がいた方が面白いし分かりやすいじゃないですか。
プログラミングも同じで、処理の中に主人公がいた方が読みやすくて扱いやすくなるわけです。

ちなみにnumというオブジェクトは以下のクラスから作成されていました。へぇ〜ぐらいで流してください。

num.rb
class Num
  attr_accessor :value, :content

  def initialize(value, content)
    @value = value
    @content = content
  end

  def calculate(number)
    self.value = value * number + 1
  end

  def update_content
    self.content = "計算結果は#{value}です"
  end

  def output
    puts content
  end
end

num = Num.new(100,"")
num.calculate(100)
num.update_content
num.output

オブジェクトとは

先ほどの例で少しはオブジェクト指向の雰囲気を掴めたかと思います。
でば、オブジェクトとは一体なんでしょうか?

こいつの正体を掴むのに僕は半年ほどかかりました。そして、僕が腑に落ちた例えで分かりやすーーーく説明します。

結論から言うと、オブジェクトとは魔法の箱です。

image.png

この魔法の箱は2つの性質を持ちます。

  1. 情報を持つ
  2. 反応する

プログラミングをやっていると、オブジェクトが色んなことをしているように見えますが、結局やっていることはこの2つだけなんです。

情報を保持するから「箱」、メッセージに対して反応できるから「魔法の」と言う例えにしました。

オブジェクトとは、情報を持ち、メッセージに対して反応するただの箱なんだ。

とりあえず今の段階ではそのように理解してもらえれば大丈夫です。

1. 情報を持つとは?

オブジェクトは情報を持つことができます。分かりやすいのが以下の例。

user.name 
=> 山田
user.age 
=> 18

これはuserという魔法の箱がnameageという2つの情報を持っていることを示しています。すごく簡単ですね。

この情報はいつでも呼び出すことができますし、その気になれば更新することも消すこともできます。

2. 反応するとは?

オブジェクトはあるメッセージに対して反応することもできます。

user.hello
=> "おはようございます。"

これまで5万回は見たhelloの例です。
これは、helloというメッセージを伝えたら"おはようございます。"と返すようにuserという魔法の箱が反応したと見ることができます。

userをレシーバ、helloをメッセージと呼ぶのはこのような理由です。

ちなみに、そこらへんのゴミ箱にhelloと話しかけても何も返事しません。

dustbox.hello
=> NameError (undefined local variable or method `dustbox' for main:Object)

userという箱が反応してくれたのは、事前に反応パターンを記憶させたからです。

class User
  def hello
    puts "おはようございます。"
  end
end

このように、魔法の箱(オブジェクト)に教え込む反応方法のことをメソッドと言います。

では、この教えこむ場所のことを(学校の)クラスと呼ベばかなり分かりやすいのではないかと閃きました。
言い換えれば、classとは魔法の箱の養成クラスとでもいいましょうか。

image.png

すごく簡単にまとめると...

  • オブジェクトとは魔法の箱である
  • 魔法の箱は、情報を持ったり、メッセージに対して反応したりできる
  • 魔法の箱に教え込む反応パターンのことをメソッドと呼ぶ
  • 反応パターンを教え込む場所のことをクラスと呼ぶ
  • オブジェクト指向とは、プログラムを「手順」ではなく、「魔法の箱(オブジェクト)の反応」と見る考え方

以上が、オブジェクト指向の説明になります。
この記事でオブジェクト指向への理解が少しでも深まれば幸いです。

追記1

Numクラスのソースコードに間違いがあったため、修正いたしました。
ご指定いただいた方々ありがとうございます。

変更点

  • Class Num => class Numへ修正
  • 属性名をnum => valueへ変更。(属性名とインスタンス名が同じだとややこしいため)
  • caluculateメソッドにおける、value = => self.value に修正(属性を上書きする際はself.の省略はできないようです。)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails で簡単!】PAY.JPを利用したクレジットカード決済の導入

Image from Gyazo

何かサービスを作る際に、決済機能を導入したいはずです。
今回はPAY.JPを利用した決済機能を案内していきます。

PAY.JPの導入準備

スクリプトの記述

PAY.JPを使うためのスクリプトを記述します。
下記をコピーしてください。

スクリプト
%script{src: "https://js.pay.jp/", type: "text/javascript"}

コピーしたら、application.html.hamlに貼り付けましょう!!

application.html.haml
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %title payjptest
    %script{src: "https://js.pay.jp/", type: "text/javascript"}
    -# このscriptを記載
    = csrf_meta_tags
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
  %body
    = yield

PAY.JPの登録

PAY.JPのアカウントを作成しましょう!
https://pay.jp/

APIキーを取得します

アカウントを作成してログインし、下記の場所のAPIキーを確認しましょう!

Image from Gyazo

Appにgem 'payjp'を追加

Gemfile
# PAY.JPのgem
gem 'payjp'

# 環境変数を簡単に定義できるENVファイルを対応させるgem
gem 'dotenv-rails'

追加したら

ターミナル
$ bundle install
$ rails s 

再起動しないとgemもscriptも読み込まれないので、エラーがおきます。

環境変数を利用して、APIキーをAppに登録

gem 'dotenv-rails'をインストールできたので、.envファイルを作成しましょう

app > .env の場所に作成します。

Image from Gyazo

.gitignoreの上だと思えば簡単です。

秘密鍵をGithubにコミットしてしまうとAPIキーを世に公開してしまうので、.gitignoreに.envを記述します

gitignore
/.env

ここ本当に重要なので、注意してくださいね!

では、APIキーを記述します

.env
PAYJP_PRIVATE_KEY     = 'sk_test_111111111111111111111111'
PAYJP_KEY             = 'pk_test_111111111111111111111111'

記載したら、Githubのコミットに表示されていないか?確認します。
コミットに表示されてたら、ヤバイです。
危険です。.gitignoreを再確認してください。

ここまでで準備完了です。

素材作成(view):ここは自身で記述すると良いかと思います。

ここはviewですが、必要なければ飛ばしてください。

クレジットカードを登録するためのviewを作成します。
下記はformの部分テンプレートですが下記のような

スクリプト

記述
#{asset_path 'creditcards/master-card.svg'}

Image from Gyazo

master-card.svgの画像素材がないとエラーがおきますので、
コピペするなら素材を集めてください。

form

formの部分テンプレート
= form_with url:creditcards_path, method: :post, html: { name: "inputForm" },class:"form" do |f|
  .form__upper
    .form__upper__group
      = f.label :カード番号
      %span.form-require 必須
      = f.text_field :card_number, name: "card_number", id:"card_number", type: "text", placeholder: '半角数字のみ', class: 'input-default', maxlength: "16"
      %ol
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/visa.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/master-card.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/saison-card.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/jcb.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/american_express.svg'}", width:"35", height:"20", class:"american_express"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/dinersclub.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/discover.svg'}", width:"35", height:"20"}
    .form__upper__group.exp
      .name
        = f.label :有効期限
        %span.form-require 必須
      = f.select :exp_year, [["19",2019],["20",2020],["21",2021],["22",2022],["23",2023],["24",2024],["25",2025],["26",2026],["27",2027],["28",2028],["29",2029]], {}, class: 'input-default harf', name: "exp_year", id:"exp_year"
      %span= 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: 'input-default harf', name: "exp_month", id:"exp_month"
      %span.form__upper__group
      = f.label :セキュリティーコード
      %span.form-require 必須
      = f.text_field :cvc, name: "cvc", id:"cvc", class:"cvc", type: "text", placeholder: 'カード背面4桁もしくは3桁の番号', class: 'input-default', maxlength: "16"
    .form__upper__group
      %p.about
        = fa_icon 'question-circle'
        %span
        = link_to 'カード裏面の番号とは?', root_path, class:'about__registered'
    .form__upper__group
      = f.submit '次へ進む', class: 'btn-default', id: "charge-form"

data: "#{asset_path 'creditcards/visa.svg'}"は、
【 app > asset > images > creditcards > visa.svg 】を読み込む
という設定になります。

scss
.form{
  &__upper{
    margin: 0 auto;
    max-width: 343px;
    p {
      text-align: center;
    }
    &__group {
      font-size: 14px;
      color: #333;
      &:not( :first-child ){
        margin-top: 32px;
      }
      label{
        font-weight: 600;
      }
      ol{
        display:flex;
        li {
          margin: 5px 8px 0 0;
        }
      }
      .form-require {
        background-color: $green;
        color: #fff;
        font-size: 12px;
        margin: 0 0 0 8px;
        padding: 2px 4px;
        border-radius: 2px;
        vertical-align:top;
        &-optional {
          background-color: gray;
          color: #fff;
          font-size: 12px;
          margin: 0 0 0 8px;
          padding: 2px 4px;
          border-radius: 2px;
          vertical-align:top;
        }
      }
      .input-default{
        width: 90%;
        margin: 8px 0 0;
        height: 48px;
        padding: 10px 16px 8px;
        border-radius: 4px;
        border: 1px solid #ccc;
        background: #fff;
        line-height: 1.5;
        font-size: 16px;
        &.harf{
          width: calc(40% - 6px);
          margin: 8px 8px 0 0;
          height: 45px;
          border-radius: 4px;
          border: 1px solid #ccc;
          background: #fff;
          line-height: 1.5;
          font-size: 16px;
          &.exp{
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
          }
        }
        &-select{
          width: 76px;
          margin-top: 8px;
          height: 45px;
          border-radius: 4px;
          border: 1px solid #ccc;
          background: #fff;
          line-height: 1.5;
          font-size: 16px;
        }
      }
      h3 {
        font-size: 16px;
        font-weight: bold;
      }
      .attention{
        margin: 8px 0 0;
      }
      .agree{
        text-align: center;
      }
      a {
        color: #0099e8;
        text-decoration: none;
      }
      span {
        margin: 0 2px;
      }
      .about{
        text-align: right;
        &__registered{
          color: #0099e8;
          text-decoration: none;
        }
        .fa-chevron-right {
          color: #0099e8;
        }
        .fa-question-circle {
          color: #0099e8;
          font-size: 1rem
        }
      }
    }
    .form-info-text {
      color: #888;
      margin-top: 8px;
      font-size: 14px;
    }
  }
  &__bottom {
    margin: 0 auto;
    max-width: 343px;
    .registance {
      text-align: right;
    }
  }
  .btn-default {
    width: 100%;
    height: 50px;
    background-color: $green;
    color: #FFFFFF;
    font-size: 15px;
    cursor: pointer;
  }
  .btn-registration{
    color: #fff;
    border-radius: 4px;
    width: 50%;
    line-height: 48px;
    border: 1px solid transparent;
    text-align: center;
    margin: 0 auto;
    position: relative;
    i{
      font-size:20px;
      position: absolute;
      top:13.5px;
      left:15px;
    }
    .about__registered {
      color: #FFFFFF;
      text-decoration: none;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      cursor: pointer;
    }
    &.email{
      background-color: $green;
    }
    &.facebook{
      background-color:#385184;
    }
    &.google{
      background-color:#FFFFFF;
      color: black;
      background: #fff image-url('google.svg') 
      no-repeat 3px top;
      border: #979797 solid 1px;
    }
    &:not( :first-child ){
      margin-top: 16px;
    }
  }
}
new.html.haml
= render 'shared/main-header'
.container-fluid
  .row.py-5.w-100
    = render 'shared/mypage-side'
    .mypage-main.col-9
      %h2.header_title
        支払い方法
      .single-container
        .rgs-main__section
          = render "shared/creditcard-form"
        .payment-explain
          = fa_icon 'chevron-right', class:"arrows"
          = link_to '支払い方法について', '#'

モデル作成

ターミナル
$ rails g model creditcard

creditcardsテーブル

Column Type Options
user_id references foreign_key: true, null: false
payjp_id string null: false

Association

  • belongs_to :user

ということでマイグレーションファイルは下記になります。

migrationファイル
class CreateCreditcards < ActiveRecord::Migration[5.2]
  def change
    create_table :creditcards do |t|
      t.references :user,  foreign_key: true, null: false
      t.string :payjp_id, null: false
      t.timestamps
    end
  end
end

ここは他の記事と異なります。

  • user_id: AppのUser-ID
  • payjp_id: PAYJPのUser-ID

他の記事だとカード用のカラムも作成していますが、PAYJPのアカウントから引っ張りだせばいいので不要です。

本題のjQueryです。

Payjp.js
$(document).on('turbolinks:load',function(){
  // PAY.JPの公開鍵をセットします。
  Payjp.setPublicKey('pk_test_111111111111111111');

  //formのsubmitを止めるために, クレジットカード登録のformを定義します。
  var form = $(".form");

  $("#charge-form").click(function() {
    // submitが完了する前に、formを止めます。
    form.find("input[type=submit]").prop("disabled", true);
    // submitを止められたので、PAY.JPの登録に必要な処理をします。

    // formで入力された、カード情報を取得します。
    var card = {
      number: $("#card_number").val(),
      cvc: $("#cvc").val(),
      exp_month: $("#exp_month").val(),
      exp_year: $("#exp_year").val(),
    };

    // PAYJPに登録するためのトークン作成
    Payjp.createToken(card, function(status, response) {
      if (response.error){
        // エラーがある場合処理しない。
        form.find('.payment-errors').text(response.error.message);
        form.find('button').prop('disabled', false);
      }   
      else {
        // エラーなく問題なく進めた場合
        // formで取得したカード情報を削除して、Appにカード情報を残さない。
        $("#card_number").removeAttr("name");
        $("#cvc").removeAttr("name");
        $("#exp_month").removeAttr("name");
        $("#exp_year").removeAttr("name");
        var token = response.id;
        form.append($('<input type="hidden" name="payjpToken" />').val(token));
        form.get(0).submit();
      };
    });
  });
});

コントローラーの記述

コントローラー(new&create)の作成

creditcards_controller.rb
class CreditcardsController < ApplicationController
  require "payjp"
  before_action :set_card

  def new
    # cardがすでに登録済みの場合、indexのページに戻します。
    @card = Creditcard.where(user_id: current_user.id).first
    redirect_to action: "index" if @card.present?    
  end

  def create
    # PAY.JPの秘密鍵をセット(環境変数)
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]

    # jsで作成したpayjpTokenがちゃんと入っているか?
    if params['payjpToken'].blank?
      # トークンが空なら戻す
      render "new"
    else
      # トークンがちゃんとあれば進めて、PAY.JPに登録されるユーザーを作成します。
      customer = Payjp::Customer.create(
        description: 'test',
        email: current_user.email,
        card: params['payjpToken'],
        metadata: {user_id: current_user.id}
      )

      # PAY.JPのユーザーが作成できたので、creditcardモデルを登録します。
      @card = Creditcard.new(user_id: current_user.id, payjp_id: customer.id)
      if @card.save
        redirect_to action: "index", notice:"支払い情報の登録が完了しました"
      else
        render 'new'
      end
    end
  end

  private
  def set_card
    @card = Creditcard.where(user_id: current_user.id).first if Creditcard.where(user_id: current_user.id).present?
  end
end

コントローラーの追記(index、destory)

creditcards_controller.rb
class CreditcardsController < ApplicationController
  require "payjp"
  before_action :set_card

  def index
    # すでにクレジットカードが登録しているか?
    if @card.present?
      # 登録している場合,PAY.JPからカード情報を取得する
      # PAY.JPの秘密鍵をセットする。
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      # PAY.JPから顧客情報を取得する。
      customer = Payjp::Customer.retrieve(@card.payjp_id)
      # PAY.JPの顧客情報から、デフォルトで使うクレジットカードを取得する。
      @card_info = customer.cards.retrieve(customer.default_card)
      # クレジットカード情報から表示させたい情報を定義する。
      # クレジットカードの画像を表示するために、カード会社を取得
      @card_brand = @card_info.brand
      # クレジットカードの有効期限を取得
      @exp_month = @card_info.exp_month.to_s
      @exp_year = @card_info.exp_year.to_s.slice(2,3) 

      # クレジットカード会社を取得したので、カード会社の画像をviewに表示させるため、ファイルを指定する。
      case @card_brand
      when "Visa"
        @card_image = "visa.svg"
      when "JCB"
        @card_image = "jcb.svg"
      when "MasterCard"
        @card_image = "master-card.svg"
      when "American Express"
        @card_image = "american_express.svg"
      when "Diners Club"
        @card_image = "dinersclub.svg"
      when "Discover"
        @card_image = "discover.svg"
      end
    end
  end

  def new
    @card = Creditcard.where(user_id: current_user.id).first
    redirect_to action: "index" if @card.present?    
  end

  def create
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
    if params['payjpToken'].blank?
      render "new"
    else
      customer = Payjp::Customer.create(
        description: 'test',
        email: current_user.email,
        card: params['payjpToken'],
        metadata: {user_id: current_user.id}
      )
      @card = Creditcard.new(user_id: current_user.id, payjp_id: customer.id)
      if @card.save
        if request.referer&.include?("/registrations/step5")
          redirect_to controller: 'registrations', action: "step6"
        else
          redirect_to action: "index", notice:"支払い情報の登録が完了しました"
        end
      else
        render 'new'
      end
    end
  end

  def destroy     
    # 今回はクレジットカードを削除するだけでなく、PAY.JPの顧客情報も削除する。これによりcreateメソッドが複雑にならない。
    # PAY.JPの秘密鍵をセットして、PAY.JPから情報をする。
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
    # PAY.JPの顧客情報を取得
    customer = Payjp::Customer.retrieve(@card.payjp_id)
    customer.delete # PAY.JPの顧客情報を削除
    if @card.destroy # App上でもクレジットカードを削除
      redirect_to action: "index", notice: "削除しました"
    else
      redirect_to action: "index", alert: "削除できませんでした"
    end
  end

  private
  def set_card
    @card = Creditcard.where(user_id: current_user.id).first if Creditcard.where(user_id: current_user.id).present?
  end
end

登録したカード情報を表示

Image from Gyazo
index.html.hamlに記述します。

index.html.haml
.mypage-main.col-9
      %h2.header_title
        支払い方法
      .single-container
        %section.creditcard_section
          %h3 クレジットカード一覧
          - if @card.present?
            .container
              .creditcard-info
                = image_tag "creditcards/#{@card_image}",width:'34',height:'20', alt:'master-card'
                %p.creditcard-info__number
                  = "**** **** **** " + @card_info.last4 #クレジットカードの下4桁を表示
                %p.creditcard-info__period 
                = @exp_month + " / " + @exp_year
                = button_to "削除する", creditcard_path(@card), method: :delete, class:"creditcard-info__delete"
          - else
            .new-card
              = link_to new_creditcard_path, class:"new-card-btn" do
                %i.far.fa-credit-card 
                クレジットカードを追加する

クレジットカードがない場合は下記の表示にさせます
Image from Gyazo

購入処理を追加

creditcards_controller.rb
def buy
    @product = Product.find(params[:product_id])
    # すでに購入されていないか?
    if @product.buyer.present? 
      redirect_back(fallback_location: root_path) 
    elsif @card.blank?
      # カード情報がなければ、買えないから戻す
      redirect_to action: "new"
      flash[:alert] = '購入にはクレジットカード登録が必要です'
    else
      # 購入者もいないし、クレジットカードもあるし、決済処理に移行
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      # 請求を発行
      Payjp::Charge.create(
      amount: @product.price,
      customer: @card.customer_id,
      currency: 'jpy',
      )
      # 売り切れなので、productの情報をアップデートして売り切れにします。
      if @product.update(buyer_id: current_user.id)
        flash[:notice] = '購入しました。'
        redirect_to controller: 'products', action: 'show', id: @product.id
      else
        flash[:alert] = '購入に失敗しました。'
        redirect_to controller: 'products', action: 'show', id: @product.id
      end
    end
  end

あとはボタンを押したら、buyアクションが動くようにすれば、完了です!!

以上です
お疲れ様です。

参考リンク

新規登録時にクレジットカード登録

購入処理

テストカード

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

Rails+jQuery イベントの発火により部分テンプレートを更新させる方法

はじめに

インクリメンタルサーチについて書いた記事ではajaxとjsonの組み合わせを使ってビューを変化させていましたが、今回はjsonを使わずRailsの部分テンプレートを更新させる方法でビューを変更する方法について自分なりにまとめていきます。

環境

Ruby 2.5.1
Rails 6.0.2
haml、jQueryを使っていきます。

やりたいこと

form_withのsubmitを使わず、jsのイベント発火(clickやchangeなど)により非同期通信で部分テンプレートを更新させたいです。
jsonを活用して更新する方法もありますが、ここでは部分テンプレートを活用して、js内の記述を簡略化させていきます。

まずはsubmitを使った時の更新

post_controller.rb
class PostController < ApplicationController
  def index
    @posts = Post.all
  end

  def create
    @post = Post.create(message: params[:message])
    @posts = Post.all
    render partial: "post", collection: @posts
  end
end
index.html.haml
%h2
  Posts

= form_with model: @post do |f|
  = f.text_field :message
  = f.submit "Post", id: :submit

.posts
  = render partial: "post", collection: @posts
_post.html.haml
= post.message

pict1.png

まずは比較として、jsを使わず更新してみます。

結果

pict2.png
部分テンプレートのみの更新ではなく、ページ全体を部分テンプレートに変更されてしまいました。

部分テンプレートのみ更新させる

post_controller_rb
class PostController < ApplicationController
  def index
    @posts = Post.all
  end

  def create
    @post = Post.create(message: params[:message])
    @posts = Post.all
    #  partial: "post", collection: @posts  <- 削除
  end
end
create.js.haml(新規作成)
$('.posts').html("#{j(render partial: "post", collection: @posts)}");

結果

pict3.png
pict4.png
部分テンプレートのみ更新させることができました。

本題 submitでの送信ではなく、jsのイベントで部分テンプレートの更新を行う

post.js
$(function() {
  $('#submit').on('click', function(event) {
    event.preventDefault();
    var input = $('#message').val();
    console.log(input)
    $.ajax({
      type: "POST",
      url: "/post",
      data: {message: input}
    })
    .done(function(response) {
      $('.posts').html(response);
    })
  })
})
post_controller.rb
class PostController < ApplicationController
  def index
    @posts = Post.all
  end

  def create
    @post = Post.create(message: params[:message])
    @posts = Post.all
    render partial: "post", collection: @posts  <- 復元
  end
end

サーバーから返されるデータの形式を指定しないことで、標準的なHTMLで記述された部分テンプレートの情報が返されてきているようです。

結果

pict5.png
jsで更新させる部分にajax通信で返されたデータを置換させることで部分テンプレートの更新が実現できました。

活用できそうな場面

マウスオーバーと組み合わせてマウスの位置によってビューを更新させたり、一時的に処理を停止させたりといったjsとの組み合わせで活用できそうです。

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

【Ruby on Rails】gem 'ransack' を利用した検索機能/フォームの実装

検索機能をgemを使って簡単に実装しましょう!

gem 'ransack'を利用した検索機能実装

Image from Gyazo

インストール

Gemfile
gem 'ransack'

インストールしましょう

ターミナル
$ bundle install

検索機能の作成

application_controller.rb
before_action :set_search

def set_search
  @search = Product.ransack(params[:q]) #ransackの検索メソッド
  @search_products = @search.result(distinct: true).order(created_at: "DESC").includes(:user).page(params[:page]).per(5) # productsの検索結果一覧 
   # 最終的に、@search_productsを検索結果画面(例:search.html.haml)に挿入します。
   # 検索結果の一覧:  @search_products = @search.result.order(created_at: "DESC")
   # distinct: trueは検索結果のレコード重複しないようにします。
   # ページネーション:  .includes(:user).page(params[:page]).per(5
end

application_controller.rbで定義します。

products_controller.rbに書くとproducts_controller.rbを使わないページを表示した時にエラーになるため、application_controller.rbで記述。
kaminariでページネーションをつけているのでpage(params[:page])をつけた。
searchメソッドよりransackメソッド推奨なのでransackメソッドを使った。

ransackの検索オプション

検索方法 意味(英語) 意味
*_eq equal 等しい
*noteq not equal 等しくない
*_lt less than より小さい
*_lteq less than or equal より小さい(等しいものも含む)
*_gt grater than より大きい
*_gteq grater than or equal より大きい(等しいものも含む)
*_cont contains value 部分一致(内容を含む)

今回使ったのは*_contですね

distinct: trueの参考になるページ

検索フォームの作成

検索.html.haml
.search_wap
   = search_form_for @search do |f|
     = f.text_field :name_cont , placeholder: "何かお探しですか?", class: 'input'
       = button_tag type: 'submit', class: 'search__button' do
         .icon_wap
           %i.fas.fa-search

haml形式でフォームを作成した。
この段階では、url:〇〇_pathはつけていません。あとで付与していきます。
scssのmixinも記載しておきます。

mixin_search
@mixin input{
  border: none;
  border-radius: 50px;
  background: #F4F8F9;
  height: 37px;
  width: 100%;
  padding: 10px 20px;
}

.search_wap{
  margin-right: 15px;
  position: relative; 
  .input{
    @include input;
    margin: 13px 0 0;
  }
  .search__button{
    background-color: transparent;
    cursor: pointer;
    position: absolute;
    right: 0px;
    top: 13px;
    height: 37px;
    width: 40px;
    border-style: none;
    border-radius: 0px 4px 4px 0px;
    padding: 0px;
    .icon_wap{
      i{
        position: absolute;
        right: 14px;
        top: 13px;
        font-size: 13px;
        font-weight: 900;
        color: #999999;
      }
    }
  }
}

検索結果のview作成

あとは、検索画面を作成して、検索結果である@search_productsを挿入してあげれば完成です。

検索結果用の画面に移行するために、route.rbにget :searchを追加します。

route.rb
Rails.application.routes.draw do
  resources :products do
    collection do
      get :search
    end
  end
end

では、検索フォームにurlを追加しましょう

検索.html.haml
.search_wap
   = search_form_for @search, url: search_users_path do |f|
     = f.text_field :name_cont , placeholder: "何かお探しですか?", class: 'input'
       = button_tag type: 'submit', class: 'search__button' do
         .icon_wap
           %i.fas.fa-search

view用のhtml.hmlとコントローラーを記述します。

products_controller.rb
class UsersController < ApplicationController
  def search
  end
end

search.html.haml
= render 'shared/main-header'
.container-fluid
  .row.py-5.w-100
    .jscroll
      = render partial: 'user', collection: @search_products, as: "product", class: "jscroll"

// @search_productはapplication_controller.rbで定義している。

@search_productで検索結果のProductを表示させるので、indexで作成したviewに@products@search_productに書き換えれば、出来上がりです。

まとめ

検索処理は下記だけ

application_controller.rb
before_action :set_search
def set_search
  @search = Product.ransack(params[:q]) #ransackの検索処理
  @search_products = @search.result # 検索結果
end

検索フォームを作って

検索.html.haml
.search_wap
   = search_form_for @search do |f|
     = f.text_field :name_cont , placeholder: "何かお探しですか?", class: 'input'
       = button_tag type: 'submit', class: 'search__button' do
         .icon_wap
           %i.fas.fa-search

あとはviewに@search_productsを挿入すれば完成。

if文を追加する

if文を追加するなら、下記の3パターンがいいかもしません

// 検索キーワードなし: 空白だとスペースを入れいてるname全部がヒットしてしまうため
- if params[:q]['name_cont'] == ""
  = "検索キーワードがありません。"

// 検索結果ありの場合
- elsif @search_products.present?
  = "「#{params[:q][:name_cont]}」の検索結果: #{@search_products.count}個"

// 検索数0の場合
- else
  = "検索に一致する商品はありませんでした"

では実際に書いていきましょう!

参考リンク

ransackでRailsアプリのヘッダーに検索機能をつける
【Rails】ヘッダーへの検索機能の付け方
Rubyon Rails で検索機能を作ろう(ransack)
[Rails]ransackを利用した色々な検索フォーム作成方法まとめ
Rails 5.1とBootstrapで作るシンプルな検索機能のテンプレ

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

Railsを用いたアプリケーション開発の流れ

はじめに

Railsを用いてアプリケーション開発をするための流れを簡潔にまとめたいと思います。

概要

以下の流れで実装を行い、トップページにデータベースに保存してあるデータを表示する機能を作成します。
1.アプリケーションの土台となるものを作る
2.データベースを作る
3.ルーティングの設定
4.コントローラの作成
5.ビューの作成
6.モデルの作成
7.ビューにインスタンス変数を埋め込む

1.アプリケーションの土台となるものを作る

まずはアプリケーション用のディレクトリを作成してください。
そのディレクトリの直下で以下のコマンドを入力します。x.x.xの部分はrailsのバージョンを入れてください。
rails _x.x.x_ new アプリケーション名 -d 使用するデータベース
これでコマンドを実行したディレクトリの直下に様々なファイルが作成されたはずです。これがアプリケーションの土台となります。

2.データベースを作る

以下のコマンドでアプリケーションで使用するデータベースを作成する事ができます。
rails db:create
実際にデータベースを確認して見ましょう。「アプリケーション名_development」と「アプリケーション名_test」というデータベースが作成されているはずです。

3.ルーティングの設定

「1.アプリケーションの土台となるものを作る」を実行していれば、/アプリケーション用のディレクトリ/config/app の配下にroutes.rbというファイルが作成されていると思います。このファイルに「root to: "posts#index"」を追記しましょう。

Rails.application.routes.draw do
  root to: "posts#index"
end

これで「root」ディレクトリにリクエストがあった場合、
「posts」コントローラに処理が遷移し、
postsコントローラの「index」アクションの処理を行う、となります。

4.コントローラの作成

以下のコマンドでコントローラーを作成できます。「3.ルーティングの設定」でpostsコントローラに処理が遷移するとしたため、今回はコントローラー名をpostsにしましょう。
rails g controller コントローラー名

5.ビューの作成

今回は、postsコントローラのindexアクションに対応するビューであるため、/アプリケーション用のディレクトリ/app/views/posts の配下にindex.html.erbを作成しましょう。コントローラから受け取るインデックス変数などの処理は後ほど記載します。

6.モデルの作成

ターミナルで以下のコマンドを実行しましょう。
これでモデル名をpostにすればpostsテーブルに対応するモデルができます。
rails g model モデル名

7.ビューにインスタンス変数を埋め込む

1~6までの流れでテーブルからデータを抽出する事ができ、@posts(インスタンス変数)にデータが格納されました。あとはビューを修正してインスタンス変数を画面に出力するようにすれば完成です。

# 修正例
<% @posts.each do |post|%>
  <div>
    <%= post.text %>
  </div>
<% end %>

雑感

3日ほどRubyとRailsについて学び、WEBアプリケーション制作におけるはじめの一歩が踏み出せたのではないかと思います。ちなみに筆者は漫画のはじめの一歩も大好きで、ベストバウトは間柴対木村です。

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

Rails新規アプリケーションの作り方

フレームワークRailsの作成手順(ほぼ備忘録)

ディレクトリ作成からRailsのひな形作成までを簡単にアウトプット練習も兼ねて書いていく。(DB設計は割愛)

開発環境↓

  • Railsバージョン5.2.3
  • データベース MySQL

ここからはターミナルを実行してひな形を作っていく。

ターミナル
# ディレクトリ作成
$ mkdir ~/test 

Rails newコマンドで作成

ターミナル
$ rails new アプリケーション名 -オプション
test
#test-appファイルの作成
$ rails _5.2.3_ new test-app -d mysql

$ cd test-apptest-appファイルに移動

test-app
# bundle installしておく
$ bundle install

これでRailsフレームワークのひな形が完成。

htmlファイルやcssファイル、routes.rbがあることを確認しておく。

これからDBとモデルを作成する。

以上

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

[Rails] ActiveRecord::import で NoMethodError: undefined method `raise_record_invalid' が発生したときの対応

はじめに

ActiveRecord を使ってDBアクセスをしている場合、バルクインサートをやってくれる gem に activerecord-import というのがある。
とても便利なライブラリなのだが、RSpec で ActiveRecord::RecordInvalid の発生を期待するテストを書いた際に当該エラーが発生しなかったので備忘録として残す。

環境

バージョン 備考
Ruby v2.5.5p157
Ruby on Rails v5.2.3
activerecord-import v0.14.1 バルクインサートをやってくれる

なにが発生したか

前述のとおり ActiveRecord::RecordInvalid が発生するテストを書いたのだが、次のエラーが発生した。

expected ActiveRecord::RecordInvalid, got #<NoMethodError: undefined method `raise_record_invalid' for

結論から

この現象の解決には activerecord-import の update を行う。( 下記のバージョンを指定して bundle install を実行する )

gem 'activerecord-import', '~> 0.15.0'

発生していた理由

activerecord-import のバグ。下記で fix の報告があった。

https://github.com/zdennis/activerecord-import/pull/294

まとめにかえて

上記のプルリクエストがマージされたのは on 3 Jul 2016 と古い。

スクリーンショット 2020-02-21 12.22.46.png

そういうわけなので、最近導入した方は本記事で取り上げた現象は発生していないと思われるが、導入したのが古いバージョンのままで更新をかけていないとハマるのでご注意を。

なんせ ActiveRecord::import, ActiveRecord::RecordInvalid, NoMethodError, undefined method raise_record_invalid といったキーワードで検索かけても全然糸口が見つからない。
そんななか、検索でひっかかった こちら前掲のプルリクエストがあって気づけたのは幸いでした。。。

参考

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