20200820のRailsに関する記事は28件です。

Railsチュートリアル 5章最後でHerokuが動かなくなる

Railsチュートリアルをもりもりとやっています。
5章に来て初めて本格的に躓いてしまったので、備忘録として記録します。

herokuが動かない・・・

5章の最後に'git push heroku'をしたところこの画面がでました。
スクリーンショット 2020-08-20 20.25.50.png

Application error
An error occurred in the application and your page could not be served. 
If you are thee application owner, check your logs for details. 
You can do this from the Heroku CLI with the command
heroku logs --tail

対処

ひとまず色々検索して以下のコマンドを打ちました。

heroku logs herokuのログの出力
見るべきポイント:errorと書かれている行

>>$ heroku logs #ログ出力
:
(中略) #errorと書かれている箇所を探す
:
2020-08-20T12:51:51.649616+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/" host=enigmatic-harbor-30759.herokuapp.com request_id=6cf41616-a067-4dc6-8d2c-696895e1b968 fwd="60.76.208.83" dyno= connect= service= status=503 bytes= protocol=https
:

heroku run rails c railsでコンソールを起動する
見るべきポイント:エラーになっているファイル、エラー原因

>>$ heroku run rails c #コンソールの起動
:
(中略) #ファイル名が書かれている箇所を探す
:
4: from /app/app/helpers/application_helper_test.rb:1:in `<top (required)>'
: 
:#最終的なエラーを確認する
/app/vendor/bundle/ruby/2.6.0/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:292:in `require': No such file to load -- test_helper.rb (LoadError)

以上の内容でわかったこと
logを見る限りだと、よくわからない・・・
・console起動でみると、どうやらapplication_helper_test.rbが悪さをしている?
・上のファイルがtest_helper.rbを呼んでるけど、ファイルがないと言われている。

結論

application_helper_test.rbtest_helper.rbを参照できないようフォルダ構成になっていまいました。
本来であれば、testsフォルダ内にあるべきapplication_helper_test.rbファイルが別のフォルダ(app)に入っていたようです。
ひとまず、ファイルを移動させ、再度実行するとエラーがなくなりました。

感想

エラーが出た時の、ログの読み取りがうまく出来なかったのでてこずってしまいました・・・。改めて整理してみて、案外読み取るポイントってシンプルなんだなと改めておもいました。

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

privateメソッドとprotectedメソッド

【概要】

1.結論

2.”private”と”protected”とは何か

3.どのように”private”と”protected”を使うのか

4.ここから学んだこと

1.結論

状況に応じて使い分けるが、ほぼprivateメソッドを使えばいい!

2.”private”と”protected”とは何か

publicは制限がないメソッドですが、private,protectedの違いがわからず悩みました。

private:
クラス外からは呼び出せない。同じインスタンス内で呼び出せる。

protected:
クラス外からは呼び出せない。同じインスタンス内で呼び出せる。しかし、他のインスタンスであれば同じクラスであれば呼び出せる。

ということですが、3.の例を見て
理解していきましょう。

3.どのように”private”と”protected”を使うのか

private
class saturday

 def  day
  holiday
 end

 private

 def holiday
  puts "今日は休日です"
 end
end

human=Human.new
human.holiday
protected
class school

  def class (student)
   @student = student
  end

  def brother(human)
   puts "#{class_brother}#{human.class_brother}は兄弟です。"
 end
end

 protected
  def class_brother
   @student
  end

studentA=Student.new
studentA.school("桜木")
studentB=Student.new
studentB.school("山田")

studentA.brother("山田 ")

もちろんprivateのプログラムでも、
protectedが適用できます!
ただ、protectedの方は、同じクラス内でも
違うメソッド(他のインスタンスであれば同じクラスであれば呼び出せる)のため、protectedのみでしか適用できません。

4.ここから学んだこと

i)privateは使用頻度が高いと思いますが、protectedは使う範囲・頻度が限られそうです。

ii)邪魔されたくないメソッドや、影響されたくないメソッド(インスタンス変数)を使う時にはものすごく便利なのでこれからも使いつつ、プログラムを学んでいきます!

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

RuboCopの導入と基本的な使用方法【ポートフォリオ備忘録①】

はじめに

現在、ポートフォリオとしてオリジナルアプリを作成中ですが、自分一人でコードを書いているとついつい自分が読めれば問題ないコードを書きがちになってしまいます。

変な癖が付かないように、その辺も意識していきたいということで、RuboCopを導入することにしました。導入で学んだことを忘れないようにここに綴ります。

RuboCopとは

RuboCopとは、Ruby向けの静的解析ツールである。Ruby Style Guideのコーディング規約に従って、コードが書かれているかチェックするためのgemである。
参考:RuboCop // Docs

RuboCopをインストール

Gemfileに記入

Gemfile
group :development do
  gem 'rubocop', require: false
  gem 'rubocop-rails', require: false
end

インストール

terminal
$ bundle

RuboCopを使ってみる

terminal
$ bundle exec rubocop

(余談ですが、bundle execって何?という時は bundler、bundle execについて ※自分用メモ の記事がオススメ。)

なんだかエラーがたくさん出てしまった・・・
直さなければいけないところが書かれているので親切だけど、全部直すには時間がかかり過ぎてしまう。

ということで以下のコマンドを実行

$ rubocop --auto-gen-config

すると、.rubocop_ymlと.rubocop_todo.ymlの2つのファイルが生成される。

.rubocop_todo.yml

.rubocop_todo.ymlファイルの生成によって、先ほど大量に出た警告が一旦無視されるように設定される。すなわち.rubocop_todo.ymlに書かれているかぎり、RuboCopに怒られなくて済むということである。

試しにもう一度 budle exec rubocop を実行すると、警告が出なくなっている。

とはいえあくまでも今は一時的に警告を回避しているに過ぎないので、このファイルに何も書かれていなくてもRuboCopから警告文が表示されない状況に持っていくことが最終的な目標となる。

警告をなくす

rubocop_todo.yml
Bundler/OrderedGems:
  Exclude:
    - 'Gemfile'

Layout/SpaceInsideArrayLiteralBrackets:
  Exclude:
    - 'config/environments/production.rb'

Layout/TrailingEmptyLines:
  Exclude:
    - 'Gemfile'

例えば、.rubocop_todo.ymlに上記のような記載があったら、まずは

Bundler/OrderedGems:
 Exclude:
  - 'Gemfile'

をコメントアウトして、bundle exec rubocop を実行。すると警告文が出る。

このあとの手順は2パターンある。

①警告に従って、コードを修正する
②コードは変えず、警告のデフォルト変更・もしくは今後警告を出さないように設定する

①は文章通り、該当ファイルを開いて、コードを修正するということ。

.rubocop.yml

②については、先ほど .rubocop_todo.ymlと同時に生成された .rubocop.ymlに設定を書いていく。

  • RuboCopの対象から外す時
rubocop.yml
inherit_from: .rubocop_todo.yml
AllCops:
  # 除外するファイル
  Exclude:
    - "config/environments/*"
    - "vender/**/*"
    - "bin/**"
    - "db/**/*"
    - "tmp/**/*"
    - "lib/**/*"
    - "Gemfile"

自分でExcludeを設定する時は、デフォルトの設定が外れてしまうため、それらも改めて明記する必要がある。

ちなみに、上記のファイルの最上部にinherit_from: .rubocop_todo.yml があるが、この一文がないと、.rubocop_todo_ymlに書かれた内容が反映されないので注意。

  • Copの無効化

Copとは、RuboCopにおける1つのルールのことである。
上から3つ目までのCopは、有効にするか無効にするかを記述しないとエラーが出るので書く。

rubocop.yml
Style/HashEachMethods:   
  Enabled: true
Style/HashTransformKeys:
  Enabled: true
Style/HashTransformValues:
  Enabled: true

# 日本語のコメントを許可する
Style/AsciiComments:
  Enabled: false

警告文に対してどちらかの対処を行ったら、コメントアウトした警告を削除する。
.rubocop_todo.ymlの中身がなくなるまでこの繰り返し。

まとめ

  • .rubocop.yml:コーディングのルールを記載
  • .rubocop_todo.yml:現在の違反部分を記載 (名前の通りToDoリスト的な役割だ!)

.rubocop_todo.ymlの違反を1つずつチェックして、コードを修正もしくは新たなルールを作って.rubocop.ymlに記載するのがRuboCopの基本的な使い方である。

おわりに

とにかく今後はRuboCopを適宜利用して、規約に沿ったコードを書いていきたいと思います。本記事で何かツッコミどころ、おかしな点などあればご教示いただけると幸いです。

尚、本記事は以下を参考にしています。

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

RuboCopの導入と基本的な使用方法【ポートフォリオ①】

はじめに

現在、ポートフォリオとしてオリジナルアプリを作成中ですが、自分一人でコードを書いているとついつい自分が読めれば問題ないコードを書きがちになってしまいます。

変な癖が付かないように、その辺も意識していきたいということで、RuboCopを導入することにしました。導入で学んだことを忘れないようにここに綴ります。

RuboCopとは

RuboCopとは、Ruby向けの静的解析ツールである。Ruby Style Guideのコーディング規約に従って、コードが書かれているかチェックするためのgemである。
参考:RuboCop // Docs

RuboCopをインストール

Gemfileに記入

Gemfile
group :development do
  gem 'rubocop', require: false
  gem 'rubocop-rails', require: false
end

インストール

terminal
$ bundle

RuboCopを使ってみる

terminal
$ bundle exec rubocop

(余談ですが、bundle execって何?という時は bundler、bundle execについて ※自分用メモ の記事がオススメ。)

なんだかエラーがたくさん出てしまった・・・
直さなければいけないところが書かれているので親切だけど、全部直すには時間がかかり過ぎてしまう。

ということで以下のコマンドを実行

$ rubocop --auto-gen-config

すると、.rubocop_ymlと.rubocop_todo.ymlの2つのファイルが生成される。

.rubocop_todo.yml

.rubocop_todo.ymlファイルの生成によって、先ほど大量に出た警告が一旦無視されるように設定される。すなわち.rubocop_todo.ymlに書かれているかぎり、RuboCopに怒られなくて済むということである。

試しにもう一度 budle exec rubocop を実行すると、警告が出なくなっている。

とはいえあくまでも今は一時的に警告を回避しているに過ぎないので、このファイルに何も書かれていなくてもRuboCopから警告文が表示されない状況に持っていくことが最終的な目標となる。

警告をなくす

rubocop_todo.yml
Bundler/OrderedGems:
  Exclude:
    - 'Gemfile'

Layout/SpaceInsideArrayLiteralBrackets:
  Exclude:
    - 'config/environments/production.rb'

Layout/TrailingEmptyLines:
  Exclude:
    - 'Gemfile'

例えば、.rubocop_todo.ymlに上記のような記載があったら、まずは

Bundler/OrderedGems:
 Exclude:
  - 'Gemfile'

をコメントアウトして、bundle exec rubocop を実行。すると警告文が出る。

このあとの手順は2パターンある。

①警告に従って、コードを修正する
②コードは変えず、警告のデフォルト変更・もしくは今後警告を出さないように設定する

①は文章通り、該当ファイルを開いて、コードを修正するということ。

.rubocop.yml

②については、先ほど .rubocop_todo.ymlと同時に生成された .rubocop.ymlに設定を書いていく。

  • RuboCopの対象から外す時
rubocop.yml
inherit_from: .rubocop_todo.yml
AllCops:
  # 除外するファイル
  Exclude:
    - "config/environments/*"
    - "vender/**/*"
    - "bin/**"
    - "db/**/*"
    - "tmp/**/*"
    - "lib/**/*"
    - "Gemfile"

自分でExcludeを設定する時は、デフォルトの設定が外れてしまうため、それらも改めて明記する必要がある。

ちなみに、上記のファイルの最上部にinherit_from: .rubocop_todo.yml があるが、この一文がないと、.rubocop_todo_ymlに書かれた内容が反映されないので注意。

  • Copの無効化

Copとは、RuboCopにおける1つのルールのことである。
上から3つ目までのCopは、有効にするか無効にするかを記述しないとエラーが出るので書く。

rubocop.yml
Style/HashEachMethods:   
  Enabled: true
Style/HashTransformKeys:
  Enabled: true
Style/HashTransformValues:
  Enabled: true

# 日本語のコメントを許可する
Style/AsciiComments:
  Enabled: false

警告文に対してどちらかの対処を行ったら、コメントアウトした警告を削除する。
.rubocop_todo.ymlの中身がなくなるまでこの繰り返し。

まとめ

  • .rubocop.yml:コーディングのルールを記載
  • .rubocop_todo.yml:現在の違反部分を記載 (名前の通りToDoリスト的な役割だ!)

.rubocop_todo.ymlの違反を1つずつチェックして、コードを修正もしくは新たなルールを作って.rubocop.ymlに記載するのがRuboCopの基本的な使い方である。

おわりに

とにかく今後はRuboCopを適宜利用して、規約に沿ったコードを書いていきたいと思います。本記事で何かツッコミどころ、おかしな点などあればご教示いただけると幸いです。

尚、本記事は以下を参考にしています。

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

RuboCopの導入と基本的な使用方法【ポートフォリオメモ①】

はじめに

現在、ポートフォリオとしてオリジナルアプリを作成中ですが、自分一人でコードを書いているとついつい自分が読めれば問題ないコードを書きがちになってしまいます。

変な癖が付かないように、その辺も意識していきたいということで、RuboCopを導入することにしました。導入で学んだことを忘れないようにここに綴ります。

RuboCopとは

RuboCopとは、Ruby向けの静的解析ツールである。Ruby Style Guideのコーディング規約に従って、コードが書かれているかチェックするためのgemである。
参考:RuboCop // Docs

RuboCopをインストール

Gemfileに記入

Gemfile
group :development do
  gem 'rubocop', require: false
  gem 'rubocop-rails', require: false
end

インストール

terminal
$ bundle

RuboCopを使ってみる

terminal
$ bundle exec rubocop

(余談ですが、bundle execって何?という時は bundler、bundle execについて ※自分用メモ の記事がオススメ。)

なんだかエラーがたくさん出てしまった・・・
直さなければいけないところが書かれているので親切だけど、全部直すには時間がかかり過ぎてしまう。

ということで以下のコマンドを実行

$ rubocop --auto-gen-config

すると、.rubocop_ymlと.rubocop_todo.ymlの2つのファイルが生成される。

.rubocop_todo.yml

.rubocop_todo.ymlファイルの生成によって、先ほど大量に出た警告が一旦無視されるように設定される。すなわち.rubocop_todo.ymlに書かれているかぎり、RuboCopに怒られなくて済むということである。

試しにもう一度 budle exec rubocop を実行すると、警告が出なくなっている。

とはいえあくまでも今は一時的に警告を回避しているに過ぎないので、このファイルに何も書かれていなくてもRuboCopから警告文が表示されない状況に持っていくことが最終的な目標となる。

警告をなくす

rubocop_todo.yml
Bundler/OrderedGems:
  Exclude:
    - 'Gemfile'

Layout/SpaceInsideArrayLiteralBrackets:
  Exclude:
    - 'config/environments/production.rb'

Layout/TrailingEmptyLines:
  Exclude:
    - 'Gemfile'

例えば、.rubocop_todo.ymlに上記のような記載があったら、まずは

Bundler/OrderedGems:
 Exclude:
  - 'Gemfile'

をコメントアウトして、bundle exec rubocop を実行。すると警告文が出る。

このあとの手順は2パターンある。

①警告に従って、コードを修正する
②コードは変えず、警告のデフォルト変更・もしくは今後警告を出さないように設定する

①は文章通り、該当ファイルを開いて、コードを修正するということ。

.rubocop.yml

②については、先ほど .rubocop_todo.ymlと同時に生成された .rubocop.ymlに設定を書いていく。

  • RuboCopの対象から外す時
rubocop.yml
inherit_from: .rubocop_todo.yml
AllCops:
  # 除外するファイル
  Exclude:
    - "config/environments/*"
    - "vender/**/*"
    - "bin/**"
    - "db/**/*"
    - "tmp/**/*"
    - "lib/**/*"
    - "Gemfile"

自分でExcludeを設定する時は、デフォルトの設定が外れてしまうため、それらも改めて明記する必要がある。

ちなみに、上記のファイルの最上部にinherit_from: .rubocop_todo.yml があるが、この一文がないと、.rubocop_todo_ymlに書かれた内容が反映されないので注意。

  • Copの無効化

Copとは、RuboCopにおける1つのルールのことである。
上から3つ目までのCopは、有効にするか無効にするかを記述しないとエラーが出るので書く。

rubocop.yml
Style/HashEachMethods:   
  Enabled: true
Style/HashTransformKeys:
  Enabled: true
Style/HashTransformValues:
  Enabled: true

# 日本語のコメントを許可する
Style/AsciiComments:
  Enabled: false

警告文に対してどちらかの対処を行ったら、コメントアウトした警告を削除する。
.rubocop_todo.ymlの中身がなくなるまでこの繰り返し。

まとめ

  • .rubocop.yml:コーディングのルールを記載
  • .rubocop_todo.yml:現在の違反部分を記載 (名前の通りToDoリスト的な役割だ!)

.rubocop_todo.ymlの違反を1つずつチェックして、コードを修正もしくは新たなルールを作って.rubocop.ymlに記載するのがRuboCopの基本的な使い方である。

おわりに

とにかく今後はRuboCopを適宜利用して、規約に沿ったコードを書いていきたいと思います。本記事で何かツッコミどころ、おかしな点などあればご教示いただけると幸いです。

尚、本記事は以下を参考にしています。

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

Railsで既存アプリにgretelを使用しパンくずリストを追加した。

目的

Railsで作成したフリマサイトでgretelを使ってパンくずリストを作成する。

gretel導入

https://github.com/lassebunk/gretel

document

https://www.rubydoc.info/gems/gretel

install

gem "gretel"
$ bundle install

fileの設定

設定fileを下記コマンドで作成

$ rails generate gretel:install
config/breadcrumb.rb
# Root crumb
crumb :root do
  link "Home", root_path
end

# Issue list
crumb :issues do
  link "All issues", issues_path
end

# Issue
crumb :issue do |issue|
  link issue.title, issue
  parent :issues
end

viewに設置

<% breadcrumb :issue, @issue %>

上記で各ページでどこまでのパンくずを出すのか設定する。実際にパンくずが出るのは下記を記述した箇所

<%= breadcrumbs pretext: "You are here: ", separator: " &rsaquo; " %>
  • pretext→パンくずリストの前のテキスト
  • separator→パンくずの間の区切り

親の設定

config/breadcrumb.rbのcrumbとendの間にparentを設定することで親を設定することができる。

config/breadcrumb.rb
crumb :root do
  link "Home", root_path
end

# user#index
crumb :users do
  link "Users", users_path
  parent :root
end

上記でparentにrootを設定。

リンクをつける

リンクの設定

リンクはconfig/breadcrumb.rbのcrumbとendの間にlinkを下記のように設定する。

config/breadcrumb.rb
crumb :root do
  link "Home", root_path
end

# user#index
crumb :users do
  link "Users", users_path
  parent :root
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PCが落ちてRailsがposgreSQLにつながらない 解決策

Rails でPG::ConnectionBad のエラーの解決方法

Macbook pro でスクールのrailsの課題を勧めていたらパソコンが落ちてしまった。PCを再起動してRails をターミナルで起動させようとしようとおなじみの以下を入力

% rails s

chromeでlocalhost:3000につなげてみると画像のようなエラーが発生した。
スクリーンショット 2020-08-20 19.29.23.png
SQLが停止していると思い

% brew services start postgresql

を行ったみたものの

Service `postgresql` already started, use `brew services restart postgresql` to restart.

なので

% brew services restart postgresql

を実行してみたものの、全く変化なし。
英語のサイトを参照

https://stackoverflow.com/questions/13410686/postgres-could-not-connect-to-server

rm /usr/local/var/postgres/postmaster.pid

実行したらうまく行った? 1時間かかりましたが解決してよかった。もし同じようなエラーで困っている方がいたら参考にしてください。
posgreを正常に停止させないと出てくるエラーらしいです。

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

ActionControllerエラー集(Ruby on Rails)※随時更新

NoMethodError in [コントローラー名]#[アクション名]→undefined method `[キーワード]' for #<>

→[コントローラー名]の[アクション名]に[キーワード]のメソッドが定義されていない。

Sprockets::Rails::Helper::AssetNotFound in [コントローラー名]#[アクション名]→The asset "" is not present in the asset pipeline.

unknown attribute '[カラム名]' for [テーブル名].

→[テーブル]に[カラム]が見つからない。

【原因】
 引っ張ってきたインスタンス変数やテーブルのカラムにないカラム名を指定してしまっている。

 私の場合編集画面で、

edit.html.haml
  = form_with model: @item, do |f|
    = f.file_field :[カラム名]

 で指定したカラムがテーブルに存在していませんでした。

【対処】
 ①マイグレーションファイルに指定のカラムがあるか
 ②マイグレーションファイルの指定のカラムにスペルミスはないか
 を確認

 ミスがあった場合、
 マイグレーションファイルを修正
 →rails dbコマンド周りはこちら【逆引きrails dbコマンド集】

参考記事

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

rails dbコマンド逆引き

はじめに

Ruby on Railsで開発をしていて、MySQLのDBを修正する機会が何度もあり、その都度エラーやデータの入れ直しが発生するため、コマンドの概要ともう少し掘り下げたところまでそれぞれの意味がおさらいできればと思い、記事にしました。
ご指摘等あればよろしくお願いいたします。

入門編

①モデルファイルを作成したので、DBに反映させたい→migrate

rails g modelでモデルファイル作成後、

$ rails db:migrate

②DBを作ったが、不要になったので削除したい→drop

$ rails db:drop

③1つ前に反映したDBテーブルを修正するために、migrationを戻したい→rollback

$ rails db:rollback

入門編まとめ

・migrateを実行すると、モデルファイルに基づいてマイグレーションファイルが作られる。
・作られたマイグレーションファイルを修正し、DBに反映するためには一度rollbackを実行しDB上から該当のテーブルを下げないといけない。(この際DBに入力したデータは削除される)

初級編

①現在、どのマイグレーションファイルがDB上で有効なのか確認する→migirate:status

$ rails db:migrate:status

適用されている場合はup、適用されていない場合はdownとしてファイルがターミナル上に表示される。

②任意のファイルまでrollbackを戻したい場合→rollback STEP=[ ]

$ rails db:rollback STEP=[戻したいステップ数]

参考記事:【Rails】$rails db:rollbackしたい時の間違えない手順

③作成したDBを一旦削除して再度作成する(その1)→reset

$ rails db:reset

④作成したDBを一旦削除して再度作成する(その2)→migrate:reset

$ rails db:migrate:reset

初級編まとめ

・【resetとmigrate:resetの違い】どちらもDBを一旦dropして再度作成することは同じだが、どのファイルを元に作り直すかが異なるため、migrate:resetではマイグレーションファイルを修正していればその内容も反映して作成されるが、migrate:resetではそうならないようだ。

詳しくは以下(常にmigrate:resetを使っていれば良さそうなものだが、そうもいかない時があるようだ)
参考記事:rake db:reset と rake db:migrate:reset の違い
参考記事:rails db:migrate:resetできなかったのでrails db:resetした

中級編

入力したDBの内容を消さずにカラムを修正したい→g migrationでカラム追加→db:migrate→カラム追加用のmigrationファイル削除

参考記事:Rails カラム名変更方法

まとめ

コマンド       概要 オプションその他
migrate DBを作成
drop    DBを削除
reset     drop + migrate  マイグレーションファイルの修正は反映されない 
migrate:reset  drop + migrate  マイグレーションファイルの修正も反映される
migrate:rollback 1つ前に反映したmigrateまで STEP=[戻したいステップ数]で任意のmigrateまでDBを戻す
migrate:status 現在どこまでmigrateが反映されているか確認する

参考記事

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

逆引きrails dbコマンド集

はじめに

Ruby on Railsで開発をしていて、MySQLのDBを修正する機会が何度もあり、その都度エラーやデータの入れ直しが発生するため、コマンドの概要ともう少し掘り下げたところまでそれぞれの意味がおさらいできればと思い、記事にしました。
ご指摘等あればよろしくお願いいたします。

入門編

①モデルファイルを作成したので、DBに反映させたい→migrate

rails g modelでモデルファイル作成後、

$ rails db:migrate

②DBを作ったが、不要になったので削除したい→drop

$ rails db:drop

③1つ前に反映したDBテーブルを修正するために、migrationを戻したい→rollback

$ rails db:rollback

入門編まとめ

・migrateを実行すると、モデルファイルに基づいてマイグレーションファイルが作られる。
・作られたマイグレーションファイルを修正し、DBに反映するためには一度rollbackを実行しDB上から該当のテーブルを下げないといけない。(この際DBに入力したデータは削除される)

初級編

①現在、どのマイグレーションファイルがDB上で有効なのか確認する→migirate:status

$ rails db:migrate:status

適用されている場合はup、適用されていない場合はdownとしてファイルがターミナル上に表示される。

②任意のファイルまでrollbackを戻したい場合→rollback STEP=[ ]

$ rails db:rollback STEP=[戻したいステップ数]

参考記事:【Rails】$rails db:rollbackしたい時の間違えない手順

③作成したDBを一旦削除して再度作成する(その1)→reset

$ rails db:reset

④作成したDBを一旦削除して再度作成する(その2)→migrate:reset

$ rails db:migrate:reset

初級編まとめ

・【resetとmigrate:resetの違い】どちらもDBを一旦dropして再度作成することは同じだが、どのファイルを元に作り直すかが異なるため、migrate:resetではマイグレーションファイルを修正していればその内容も反映して作成されるが、migrate:resetではそうならないようだ。

詳しくは以下(常にmigrate:resetを使っていれば良さそうなものだが、そうもいかない時があるようだ)
参考記事:rake db:reset と rake db:migrate:reset の違い
参考記事:rails db:migrate:resetできなかったのでrails db:resetした

中級編

入力したDBの内容を消さずにカラムを修正したい→g migrationでカラム追加→db:migrate→カラム追加用のmigrationファイル削除

参考記事:Rails カラム名変更方法

まとめ

コマンド       概要 オプションその他
migrate DBを作成
drop    DBを削除
reset     drop + migrate  マイグレーションファイルの修正は反映されない 
migrate:reset  drop + migrate  マイグレーションファイルの修正も反映される
migrate:rollback 1つ前に反映したmigrateまで STEP=[戻したいステップ数]で任意のmigrateまでDBを戻す
migrate:status 現在どこまでmigrateが反映されているか確認する

参考記事

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

逆引きrailsコマンド集【DB編(入門〜初級)】

はじめに

Ruby on Railsで開発をしていて、MySQLのDBを修正する機会が何度もあり、その都度エラーやデータの入れ直しが発生するため、コマンドの概要ともう少し掘り下げたところまでそれぞれの意味がおさらいできればと思い、記事にしました。
ご指摘等あればよろしくお願いいたします。

入門編

①モデルファイルを作成したので、DBに反映させたい→migrate

rails g modelでモデルファイル作成後、

$ rails db:migrate

②DBを作ったが、不要になったので削除したい→drop

$ rails db:drop

③1つ前に反映したDBテーブルを修正するために、migrationを戻したい→rollback

$ rails db:rollback

入門編まとめ

・migrateを実行すると、モデルファイルに基づいてマイグレーションファイルが作られる。
・作られたマイグレーションファイルを修正し、DBに反映するためには一度rollbackを実行しDB上から該当のテーブルを下げないといけない。(この際DBに入力したデータは削除される)

初級編

①現在、どのマイグレーションファイルがDB上で有効なのか確認する→migirate:status

$ rails db:migrate:status

適用されている場合はup、適用されていない場合はdownとしてファイルがターミナル上に表示される。

②任意のファイルまでrollbackを戻したい場合→rollback STEP=[ ]

$ rails db:rollback STEP=[戻したいステップ数]

参考記事:【Rails】$rails db:rollbackしたい時の間違えない手順

③作成したDBを一旦削除して再度作成する(その1)→reset

$ rails db:reset

④作成したDBを一旦削除して再度作成する(その2)→migrate:reset

$ rails db:migrate:reset

初級編まとめ

・【resetとmigrate:resetの違い】どちらもDBを一旦dropして再度作成することは同じだが、どのファイルを元に作り直すかが異なるため、migrate:resetではマイグレーションファイルを修正していればその内容も反映して作成されるが、migrate:resetではそうならないようだ。

詳しくは以下(常にmigrate:resetを使っていれば良さそうなものだが、そうもいかない時があるようだ)
参考記事:rake db:reset と rake db:migrate:reset の違い
参考記事:rails db:migrate:resetできなかったのでrails db:resetした

中級編

入力したDBの内容を消さずにカラムを修正したい→g migrationでカラム追加→db:migrate→カラム追加用のmigrationファイル削除

参考記事:Rails カラム名変更方法
参考記事:マイグレーションを使ったカラムの追加、削除、データ型の変更 [ 自分用メモ ]

まとめ

コマンド       概要 オプションその他
migrate DBを作成
drop    DBを削除
reset     drop + migrate  マイグレーションファイルの修正は反映されない 
migrate:reset  drop + migrate  マイグレーションファイルの修正も反映される
migrate:rollback 1つ前に反映したmigrateまで STEP=[戻したいステップ数]で任意のmigrateまでDBを戻す
migrate:status 現在どこまでmigrateが反映されているか確認する

参考記事

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

【個人アプリ開発メモ】ログアウトしようとして生じたエラーの解決

ログアウトしようとしたらエラーが出てしまった。

エラー文

undefined method `id' for nil:NilClass
posts_controller.rb
def index
  @posts = Post.all.includes(:user).order("created_at DESC").page(params[:page]).per(5)
  @post = Post.new

  #エラーが出たのはこの部分↓↓↓
  @calorie_sum = Post.where(user_id: current_user.id, created_at: Time.zone.now.beginning_of_day..Time.zone.now.end_of_day).sum(:calorie)

  gon.today_sum = @calorie_sum
  @standard = Standard.find_by(user_id: current_user.id)
  @calorie_standard = @standard.calorie
  gon.standard = @calorie_standard
  if @calorie_sum >= @calorie_standard
    @difference = @calorie_sum - @calorie_standard
  else
    @difference = @calorie_standard - @calorie_sum
  end
end

なぜこのエラーが生じたのか?

posts_controller.rb
@calorie_sum = Post.where(user_id: current_user.id, created_at: Time.zone.now.beginning_of_day..Time.zone.now.end_of_day).sum(:calorie)

エラーが出たこのコードはどういう意味かというと、

Postモデル(投稿データが入ってる)から、
今日1日のcurrent_userの投稿を検索して抽出して、calorieカラムの数値を合計した結果をcalorie_sumというインスタンス変数に代入してる。

なんでエラーになるかというと、
ログアウトするとuser_id: current_user.idのidのところがnilになってしまうから。

解決するにはどうすればいいのか?

current_user.idがnilでもエラーにならないようにするためには、
posts_controller.rbを条件分岐で書けばいい気がする。

こんな感じでどうだろうか?

posts_controller.rb
  def index
    @posts = Post.all.includes(:user).order("created_at DESC").page(params[:page]).per(5)
    @post = Post.new

    # サインインしてる時だけ今日の摂取カロリーを表示するグラフを作るためのデータを取得する下記の関数が動く
    if user_signed_in?
      @calorie_sum = Post.where(user_id: current_user.id, created_at: Time.zone.now.beginning_of_day..Time.zone.now.end_of_day).sum(:calorie)
      gon.today_sum = @calorie_sum
      @standard = Standard.find_by(user_id: current_user.id)
      @calorie_standard = @standard.calorie
      gon.standard = @calorie_standard
      if @calorie_sum >= @calorie_standard
        @difference = @calorie_sum - @calorie_standard
      else
        @difference = @calorie_standard - @calorie_sum
      end
    else
      # サインインしてない時は何も動かない
    end
  end

これにあとviewのグラフの部分を- if user_signed_in?で条件分岐して、サインインしてない時はグラフが出てこないようにしたら、無事解決しました。

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

【個人アプリ開発メモ】ArgumentError (wrong number of arguments (given 0, expected 2)):の解決

直面している問題

投稿しようとしたらできない。
なぜ?

ターミナルに以下のエラー文が表示された。

エラー文

ターミナル
ArgumentError (wrong number of arguments (given 0, expected 2)):
app/controllers/posts_controller.rb:21:in `create'

argumentとは引数のことらしい。
2つの引数がなければいけないのに、それが0になってるからダメだという。

引数が2つ必要?何のことだろう?

posts_controller.rb
def create
  @post = Post.new(post_params)  ここで止まってしまってる
  if @post.save
  redirect_back(fallback_location: root_path) # なぜredirect_to root_pathじゃダメなのかわかってない
else
  @posts = Post.includes(:user)
  flash.now[:alert] = '必須項目をしてください。' # フラッシュメッセージが出るか確認する
    redirect_back(fallback_location: root_path)
  end
end

private
  def post_params
    params.require(:post).permit(:food, :calorie, :protein, :fat, :carbo, :text, :image).merge(user_id: current_user.id)
  end

試してみたこと

binding.pryでpost_paramsの中身を確認してみよう。

[1] pry(#<PostsController>)> post_params
=> <ActionController::Parameters {"food"=>"うどん", "calorie"=>"10", "protein"=>"10", "fat"=>"10", "carbo"=>"10", "text"=>"うどんを食べました", "image"=>#<ActionDispatch::Http::UploadedFile:0x00007fc7e7b4eae0 @tempfile=#<Tempfile:/var/folders/sy/26p55v5j47s2zjd8h7vr8f6h0000gn/T/RackMultipart20200820-34304-1e1sjww.png>, @original_filename="image4.png", @content_type="image/png", @headers="Content-Disposition: form-data; name=\"post[image]\"; filename=\"image4.png\"\r\nContent-Type: image/png\r\n">, "user_id"=>17} permitted: true>

ここが原因ではないようだ。

https://teratail.com/questions/164134
によれば、

ターミナル
ArgumentError (wrong number of arguments (given 0, expected 2)):

これは呼び出し側の引数の数 = 0
メソッド側の引数の数 = 2
という意味らしい。

呼び出し側の引数の数と、メソッド側と引数の数はそれぞれどこで設定してるのだろうか?

試したこと

画像を選択せずに、画像以外を投稿してみた。
その結果、これは問題なく投稿できた。
この結果からわかったのは、やはり問題は画像関連だということ。

画像を選択して、画像も投稿しようとするとエラーが生じる。
画像を投稿しようとすると、メソッド側の引数が2になる?

解決した。原因は「え?そこ?」という感じ

image_uploader.rb
#[300, 200][100, 100]という感じで2つあるとダメ
process resize_to_fit: [300, 200][100, 100]

#1つに戻したら解決した
process resize_to_fit: [300, 200]

前に投稿画像サイズの設定で、2通りのサイズを設定したいと思って上記のように設定していたが、これが邪魔だったみたい。
https://qiita.com/naota7118/items/33943b8887a4990645a9

余計なことしなければよかった。

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

【LINE Bot】読書を手助けしてくれるコトバンクbotを作ってみた!

作ったもの

コトバンクで単語を検索して、その意味を返信してくれるLINE Botです。
辞書アプリやブラウザを立ち上げなくても、LINEで手軽に単語や言い回しの意味を調べられます。
IMG_2318.PNG

アニメのタイトルだってお手のもの!
IMG_2317.PNG

背景

読書中の単語検索は煩わしい

たとえば読書をしていて知らない単語や言い回しに遭遇した場合、大抵は「スマホでブラウザを起動」→「単語を入力」→「検索結果の一覧から適当にページを選択」→「ページ内で単語の意味を確認」という手順でその意味を確認すると思います。(Kindleの辞書機能でも、固有名詞や珍しい言い回しは意外とヒットしなかったり)

これが1冊で1〜2回の作業なら何も問題ないのですが、海外の古典文学や厨二用語全開のラノベ(謎の横文字や難読漢字だらけ!)を読む場合だと、下手をすると数ページおきに知らない言葉が出てきたりして、そのたびに「スマホでブラウザを起動」→「単語を入力」→「検索結果の一覧から適当にページを選択」→「ページ内で単語の意味を確認」→「次の単語を入力」→「検索結果の一覧から適当にページを選択」→……(以下ループ)とするのは非常に面倒です。

そこでLINE Botの登場

その点、LINE Botであれば「LINEを起動」→「Botに単語を送信」→「返答(単語の意味)を確認」で済みますし、連続して検索する場合でも「単語を入力」→「返答を確認」→「単語を入力」→……だけで済みます。

また、チャット上に検索結果のログも残るので、読書後などにあらためて単語を確認することもできて一石二鳥なのではないかと考えました!

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

Bot完成までの大まかな手順

  • LINE Developersの登録
    • Channelの作成
    • トークンの発行
    • Webhook URLの設定(Heroku上に作成したアプリのURLをあとから登録する)
  • Herokuの登録
    • Heroku上にアプリを作成
    • git push heroku master(アプリをHerokuにプッシュ)
    • トークンを環境変数として設定
  • アプリケーションの実装
    • gemの導入
    • callbackアクションに処理を記述
    • アプリをHerokuにアップ(完成!)

LINE Developersの登録

【Rails】1時間ぐらいで簡単にLINEのBot開発をしよう-アンケート集計Bot基礎-【画像付き】

1. 上の記事を参考に、Channelを作成

2. 「応答メッセージ: オフ」「Webhook: オン」に設定を変更

3. 「チャネルトークン」「チャネルシークレット」を発行

Herokuの登録

1時間でWikipedia検索できるLINE BOTをサクッと作ってみよう!

1.上の記事を参考に、Herokuに登録

2.Heroku上にアプリを作成(コマンドでもブラウザからでもOK)

3.さきほど発行した「チャネルトークン」「チャネルシークレット」をHerokuの環境変数に設定

% heroku config:set LINE_CHANNEL_TOKEN="チャネルアクセストークンの文字列" -a アプリケーション名
% heroku config:set LINE_CHANNEL_SECRET="チャネルシークレットの文字列" -a アプリケーション名

4.Line Developerのページに戻り、Webhook URLを設定

Webhook URLにhttps://作成したHerokuアプリのURL/callbackと設定し、
設定後に表示される「Webhookの利用」の項目もオンにする。

アプリケーションの実装

LINEBOT + RailsでWikipediabotを作ってみました。
Rails スクレイピング手法 Mechanizeの使い方

1.上の記事を参考にgemを導入

Gemgile
gem 'line-bot-api'
gem 'mechanize'
gem 'dotenv-rails'
% bundle install

2.ルーティングの設定

config/routes.rb
Rails.application.routes.draw do  
    post '/callback' => 'linebot#callback'  
end 

3.コントローラーの作成

% rails g controller linebot

4.コントローラーの編集

app/controllers/linebot_controller.rb
class LinebotController < ApplicationController
  require 'line/bot'
  require 'mechanize'

  # callbackアクションのCSRFトークン認証を無効
  protect_from_forgery :except => [:callback]

  def client
    @client ||= Line::Bot::Client.new { |config|
      config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
      config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
    }
  end

  def callback
    body = request.body.read

    signature = request.env['HTTP_X_LINE_SIGNATURE']
    unless client.validate_signature(body, signature)
      head :bad_request
    end

    events = client.parse_events_from(body)

    events.each { |event|
      if event.message['text'] != nil
        # LINEで送られてきた文書を取得
        word = event.message['text']
        # インスタンス生成
        agent = Mechanize.new
      end

      # ページのHTMLを取得
      page = agent.get("https://kotobank.jp/word/#{word}")

      # 要素を取得
      if page.search('.dictype.cf.daijisen').present?
        # デジタル大辞泉の項目が存在する場合の処理
        elements = page.search('.dictype.cf.daijisen .description')
      elsif page.search('.dictype.cf.js-contain-ad.daijisenplus').present?
        # デジタル大辞泉プラスの項目が存在する場合の処理
        elements = page.search('.dictype.cf.js-contain-ad.daijisenplus .description')
      else
        # デジタル大辞泉とデジタル大辞泉プラスの項目が存在しない場合の処理
        elements = page.search('.description')
      end

      # 概要を返す
      response = elements.inner_text.gsub(/(\s| )+/, '')

      case event
      # メッセージが送信された場合
      when Line::Bot::Event::Message
        case event.type
        # メッセージが送られて来た場合
        when Line::Bot::Event::MessageType::Text
          message = {
            type: 'text',
            text: response
          }
          client.reply_message(event['replyToken'], message)
        end
      end
    }

    head :ok
  end
end

botから送られてくるメッセージの前後に大量の空白があったので、
.gsub(/(\s| )+/, '')を追加して、スペースを取り除いています。

# 概要を返す
response = elements.inner_text.gsub(/(\s| )+/, '')

5.Herokuにデプロイ

% git add .
% git commit -m "linebot"
% git push heroku master 

最後に

元々はAPIの学習を目的として作ったものでしたが、せっかくなので記事としてアウトプットしました!
ご指摘などございましたら、ぜひコメントよろしくお願いいたします!

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

【Rails】Enumを使う

どんな時に使う?

カラムstatusの値が
0だったら未処理
1だったら進行中
2だったら処理済み
みたいな時に、いい感じに書くことができます。

定義

migrationファイル
def change
  add_column :orders, :status, :integer, default: 0 # デフォルト値は0に設定する
end
app/models/order.rb
class Order < ApplicationRecord
  enum status: { waiting: 0, working: 1, completed: 2 }
end

できること

インスタンスを作成

order = Order.new(
          item: 'ボールペン',
          price: 150,
          status: 'working' # 今まで「1」と数値でセットしていた部分を文字列でセットできる。DBには「1」が保存される
        )

インスタンスの中身を確認

pry(main)> order.status
=> "working"
pry(main)> order.working?
=> true 
pry(main)> order.completed?
=> false

インスタンスを更新

pry(main)> order.status
=> "working"
pry(main)> order.completed!
=> "completed"
pry(main)> order.status
=> "completed"

DBを検索

pry(main)> completed_orders = Order.completed
=> SELECT `orders`.* FROM `orders` WHERE `orders`.`status` = 2 LIMIT 11

複数カラムで同じ名前を使いたい場合

こういう場合
class Order < ApplicationRecord
  enum hoge_status: { waiting: 0, working: 1, completed: 2 }
  enum fuga_status: { waiting: 0, working: 1, completed: 2 }
end

Order.waiting?としても、hoge_statusfuga_statusのプログラムはどちらを見れば良いかわかりません。
こういった場合、_prefix: trueを使って解決します。

prefixで解決
class Order < ApplicationRecord
  enum hoge_status: { waiting: 0, working: 1, completed: 2 }, _prefix: true
  enum fuga_status: { waiting: 0, working: 1, completed: 2 }, _prefix: true
end
pry(main)> Order.hoge_status_completed?
=> SELECT `orders`.* FROM `orders` WHERE `orders`.`hoge_status` = 2 LIMIT 11
`
pry(main)> Order.fuga_status_completed?
=> SELECT `orders`.* FROM `orders` WHERE `orders`.`fuga_status` = 2 LIMIT 11

参考

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

HerokuへデプロイしたアプリのDBを操作する方法! CLEARDB_DATABASE Sequel_Pro

環境

rails 6.0.3
Mysql 0.4.4
アプリケーションはHerokuへデプロイ済

概要

Herokuへデプロイは出来たものの保存されたデータって見れないものだろうか
テストのために色々データ値を変更したり追加したりしたい

方法

1.Sequel Proをダウンロード

Mysqlデータベースを難しいコマンド等必要なくGUIで操作できるアプリ
Sequel Proダウンロードリンク

2.ターミナルでherokuの設定を確認

デプロイしたアプリケーションのディレクトリ下で(ターミナル)heroku configを実行
下記のような設定が出力される(実際には///の部分はもっと長い)

CLEARDB_DATABASE_URL:     mysql://
DATABASE_URL:             mysql2://
DB_HOSTNAME:              ///
DB_NAME:                  ///
DB_PASSWORD:              ///
DB_PORT:                  ///
DB_USERNAME:              ///
LANG:                     ///
RACK_ENV:                 ///
RAILS_ENV:                ///
RAILS_LOG_TO_STDOUT:      ///
RAILS_SERVE_STATIC_FILES: ///
SECRET_KEY_BASE:          ///

3.Sequel Proにデータベース情報を入力

sequrepro.png

一番上のタブを「標準」に設定して
2.でターミナルに出力したCLEARDB_DATABASE_URL:の情報をもとにSequel Proに入力
※ポート名は空白でOK

CLEARDB_DATABASE_URL:  mysql://ユーザー名:パスワード@ホスト/データベース?reconnect=true

次に「接続」と青いボタンを押したいところだがちょっとまった!
次回からもデータベースに接続を行うならお気に入りに追加をしておこう

そして最後に「接続をテスト」をクリックして接続テストに成功すれば完了

最後に

Sequel Proの使い方は各々で調べてみて下さい。
私みたいなSQLコマンドひとつもわからない初心者でも簡単にDB操作が出来ちゃいます。かなり便利。

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

SNS認証 facebookだけ通らなかった原因

前提

  • 今回、sns認証(google, facebook)を実装していました。
  • そこで、googleは既存のusersテーブルと紐づくのに、facebookだけ認証が通らないエラーが発生しましたので、原因を記録します。(そのため、sns認証の細かい実装方法に関しては割愛します。)
  • 原因は判明しましたが、根本的な解決には至っていませんので、あくまで原因を探す参考程度にご覧ください。

実行環境

rails 6.0.0

mysql 5.6.47

エラーの内容

 今回、snsの認証とともに、userの情報を紐付けて保存する仕様にしていました。
ところがログインしようとすると、、、
image.png
おかしい、、、
Googleでは確かにログインができるのに、Facebookでは新規登録画面へ飛ばされてしまいました。

原因

原因は、user.rbにありました。apiから送られてくるデータの検索方法がまずかったようです。
【user.rb】

  def self.from_omniauth(auth)
    sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
    # sns認証したことがあればアソシエーションで取得
    # 無ければemailでユーザー検索して取得orビルド(保存はしない)
    user = sns.user || User.where(email: auth.info.email).first_or_initialize(
      nickname: auth.info.name,
        email: auth.info.email
    )
    # userが登録済みの場合はそのままログインの処理へ行くので、ここでsnsのuser_idを更新しておく
    if user.persisted?
      sns.user = user
      sns.save
    end
    { user: user, sns: sns }
  end

 この記述の中だと、auth(apiから送られてくるユーザーデータ)の中から、:emailを参照してしまいます。
 しかし、私自信、facebookのアカウントにemailを登録していなかったため、この検索に引っかからず、snsの情報をDBに保存していないよねと判断され、弾かれていたようです。
 その後、Facebookのアカウント設定でemailを登録したところ、無事にログインできるようになりました。

今後の課題

このままだと、emailをもたないfacebookユーザーは原因がわからないまま永遠にログインできない状態になってしまいます。ですので、データ参照の仕様を工夫する必要がありそうです、、、

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

Rails × Herokuアプリ高速化のためにやった/やりたいこと

実装が比較的楽で、インパクトが大きいものをピックアップ。

Cloudflare (CDN)

JSやCSSはCDNに乗せる。他のCDNでも良いが、Cloudflareは無料だから最高。

Cloudinary (画像配信)

画像はCloudinaryで最適化して配信。画像の圧縮、適切なサイズでの配信、Webp対応等全部やってくれる。

クエリ最適化

N+1をなくす。pluckを使う。

partialを使いすぎない

partialは、保守性可読性とパフォーマンスのトレードオフ。
collectionオプションが使える場合には活用する。

ループの中の処理に気を使う

インスタンスを無駄に生成しない、など。

HTMLをgzip圧縮

Herokuだとデフォルトでgzip圧縮してくれないので、gemを使って対応。

ブラウザにキャッシュさせる期間を伸ばす

Cloudflare等の設定をいじって対応

重い処理はPumaの外に流す

例えば画像アップロードはLambda等に捌かせる

金を積む

最後はこれ

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

【個人アプリ開発メモ】画像が粗くなってしまう問題の解決

直面している問題

スクリーンショット 2020-08-20 午後1.51.56.png
こんな感じで投稿画像が粗くなってしまう

ヒントはつかめてる

画像のデフォルトのサイズは
app/uploaders/image_upload.rbのこの部分で設定できる。

image_upload.rb
process resize_to_fit: [300, 200]

これと同じ大きさで画像を表示すれば、キレイに表示できる。
逆にこれと違う大きさで画像を表示しようとすると、粗くなってしまう。

そのことがわかったら、
スクリーンショット 2020-08-20 午後2.06.26.png
こんな感じでキレイに表示できた。

しかしこれで問題が完全にクリアになったわけではない。
スクリーンショット 2020-08-20 午後2.09.35.png
こちらはプレビュー画像なのだが、こちらが少し粗くなってしまった。

複数の画像の大きさの設定したいときはどうすればいいのだろうか?

今気づいたのだが、選択する画像によってキレイになったり粗くなったりする。
これがなぜそうなるのかがまだわからない。

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

【個人アプリ開発メモ】画像のプレビュー表示

やりたいこと

アップロードしたい画像を選択したら、プレビュー表示できるようにしたい。

なぜ画像プレビュー機能が必要なのか?

フォームを作っただけでは、画像を選択してもどの画像が選択されたかアップロード前に確認することができない。

アップロードされてから違う画像をアップロードしてしまったと気づくようなことを防ぐために、この機能は必要。

どう進めるか

まず画像プレビュー用のブランチを立ち上げる。
次に「画像 rails プレビュー」で検索して参考記事を探す。

https://qiita.com/Masanori_N/items/71dbf648737f32dd8588
この記事を参考に進めていこう。

作業内容

jQueryで実装するため、jsファイルを作る。
javasciptディレクトリ直下に「image-preview.js」ファイルを設置した。

_main.html.haml
.new-post
      = form_with model: @post, id: 'new_post' do |f|
        .input-box
    (省略)
          .icon
            .image-upload
              %i.fa.fa-camera.fa-2x
              = f.file_field :image, class: 'image_upload'
            .btn-square
              = f.submit '投稿する', class: 'post-btn'
    .posts
      = render @posts

画像ファイル選択時にイベントを発火させる

まずはファイルを選択した時にイベントが発火するようにする。

image-preview.js
$(function() {
  $(function() {
    $(document).on('change', 'image_upload', function() {
      console.log('hoge');
    })
  });
});

これだとイベントは発火しなかった。

image-preview.js
$(document).on('change', '.image_upload', function() {
  console.log('hoge');
})

クラス名の指定で「.」が抜けているというミスが原因だった。
'image_upload''.image_upload'と修正したら無事イベントは発火した。

画像データの読みこみ

いよいよここから難しくなる。
よくわからない時はconsole.logで中身を確認するに限る。

image-preview.js
$(function() {
  $(function() {
    $(document).on('change', '.image_upload', function() {
      console.log('hoge');
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      console.log(file);
    })
  });
});

console.log(file);で選択したfileを取得できているか確認する。

検証画面で確認すると、

File {name: "image4.png", lastModified: 1596487703899, lastModifiedDate: Tue Aug 04 2020 05:48:23 GMT+0900 (日本標準時), webkitRelativePath: "", size: 110024, …}

と出てきたので、おそらくちゃんと画像fileを取得できている。

次にFileReaderを使ってFileReaderオブジェクトを生成する。
FileReaderとは何なのか?

https://developer.mozilla.org/ja/docs/Web/API/FileReader
によると、

FileReader オブジェクトを使うと、ユーザーのコンピューター内にあるファイル (もしくはバッファ上の生データ) をウェブアプリケーションから非同期的に読み込むことが出来ます。読み込むファイルやデータは File ないし Blob オブジェクトとして指定します。

とある。

なるほど、ローカルにある画像ファイルを非同期で読み込むことができるのか。

image-preview.js
//FileReaderオブジェクトの生成
var reader = new FileReader(); 

これもコンソール画面で確認してみよう。

FileReader {readyState: 0, result: null, error: null, onloadstart: null, onprogress: null, …}

次は

image-preview.js
//readAsDataURLで指定したFileオブジェクトを読み込む
reader.readAsDataURL(file); 

これもよくわからないのでそのままググってみよう。

https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsDataURL
ここでもMDNが参考記事として出てきた。

これもFileオブジェクトを読み込むために必要らしい。

image-preview.js
var hoge = reader.readAsDataURL(file); 
console.log(hoge);

こんな感じで変数に代入して中身を見てみた。
すると、undefinedとなった。

image-preview.js
//readAsDataURLで指定したFileオブジェクトを読み込む
var hoge = reader.readAsDataURL(file); 
console.log(hoge[0]);

これだとどうか?
これもダメだ。
これは中身を見ることはできないみたい。

image-preview.js
//読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
reader.onload = function() {
  ここに画像の読みこみ完了後の処理を書いていく
}

これもわからないので調べた。
https://developer.mozilla.org/ja/docs/Web/API/FileReader/onload

FileReader.onload プロパティは、readAsArrayBuffer や readAsBinaryString、 readAsDataURL、readAsText でのコンテンツ読み込みが完了して、利用可能になると発火する load イベント時に実行されるイベントハンドラを含みます

3回読んでも完全にはわからない。
readAsDataURLで画像ファイルの読みこみが完了して、利用可能になるとloadイベントが発火する。
というところまではわかった。

image-preview.js
//loadイベントが発火するかを調べる
reader.onload = function() {
  console.log('hoge');
}

コンソール画面でちゃんと「hoge」と出てきたので、loadイベントは問題なく発火してるとわかった。

その次にloadイベントの結果を取得する。

image-preview.js
//直前に実行したイベントが返した値を取得する
var image = this.result; 

プレビュー用のHTMLを設置します。
ここが結構時間かかるかな?と思っていたが、一発でいけました!!

スクリーンショット 2020-08-20 午前11.49.47.png

ここまでのコードはこんな感じです。

image-preview.js
$(function() {
  $(function() {

    // プレビューHTML生成
    function buildHTML() {
      var html = `<div class="preview-box">
                    <div class="upper-box">
                      <img src="" alt="preview" class="upload-image">
                    </div>
                    <div class="lower-box">
                      <div class="delete-box">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`;
      return html;
    }

    $(document).on('change', '.image_upload', function() {
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      //FileReaderオブジェクトの生成
      var reader = new FileReader(); 
      //readAsDataURLで指定したFileオブジェクトを読み込む
      reader.readAsDataURL(file); 
      //読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
      reader.onload = function() {
        console.log('hoge');
        //直前に実行したイベントが返した値を取得する
        var image = this.result;
        // プレビュー用のhtmlを追加
        var html = buildHTML();
        $('.text').append(html);

        //画像を追加
        $(`.upper-box img`).attr('src', `${image}`);
      }
    })
  });
});

プレビュー画像のサイズ変更

image-preview.js
// プレビューHTML生成
    function buildHTML() {
      var html = `<div class="preview-box">
                    <div class="upper-box">
                      <img src="" alt="preview" class="upload-image" height="100px" width="100px">
                    </div>
                    <div class="lower-box">
                      <div class="delete-box">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`;
      return html;
    }

<img src="" alt="preview" class="upload-image" height="100px" width="100px">
imgタグのところにheightとwidthを追加すればOK?

どのクラスに設定するかが大事で、親クラスに設定してもサイズは変わらないので注意です

プレビュー画像の削除

「削除」を押したらプレビュー画像が消えるようにしたい。

参考記事:https://qiita.com/gakinchoy7/items/f52577d0c5f6b2edff89#2-%E3%83%97%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%81%AE%E5%89%8A%E9%99%A4

まず「削除」を押した時にイベントが発火するようにする。

image-preview.js
$(document).on("click", '.delete-box', function(){
  console.log('hogehoge');
})

これで問題なく発火した。
クラス名の指定'.delete-box'が合っていればこれで発火する。

削除はremoveメソッドを使えばOK。
久しぶりだから( )をつけるのを忘れていた?

image-preview.js
$(document).on("click", '.delete-box', function(){
  $('.preview-box').remove();
})

これだけだと、プレビュー画像は消えたが、まださっきの画像ファイルを選択したままになってる。

image-preview.js
    // 「削除」を押すと削除イベントが発火する
    $(document).on("click", '.delete-box', function(){
      // プレビュー画像を削除
      $('.preview-box').remove();
      // inputタグに入ってる画像ファイルも削除
      $('.image_upload').val("");
    })

inputタグに入ってる画像ファイルも削除しておこう。
これでプレビュー画像を削除すると、ファイルの中身が空っぽになる。

2回連続ファイルを選択するとプレビュー画像が2つ表示されてしまう問題の解決

こんな感じで表示されてしまう。
これを選択するたびに入れ替わるように修正したい。
スクリーンショット 2020-08-20 午後0.53.32.png
どうすればこの問題を解決できるか?

考えたのは、

  • すでにプレビュー画像がある場合
  • プレビュー画像がない場合

に条件分岐すればいいのでは?

  • プレビュー画像あり → プレビュー画像を一度削除する必要あり
  • プレビュー画像なし → ここまで書いてきたコードでOK

どうやってプレビュー画像がある場合とない場合の条件分岐をするか?

image-preview.js
if($('.preview-box').length == 0){
  プレビュー画像がない場合の処理
}else{
  プレビュー画像がある場合の処理
}

こんな感じでlengthプロパティを使って条件分岐しました。

image-preview.js
        // プレビュー画像がまだ場合
        if($('.preview-box').length == 0){
          // プレビュー用のhtmlを追加
          var html = buildHTML();      
          $('.icon').before(html);

          //画像を追加
          $(`.upper-box img`).attr('src', `${image}`);

        // すでにプレビュー画像が存在する場合
        }else{

          // プレビュー画像を削除 ⇦ 違うのはここです
        $('.preview-box').remove();

        // プレビュー用のhtmlを追加
        var html = buildHTML();      
        $('.icon').before(html);

        //画像を追加
        $(`.upper-box img`).attr('src', `${image}`);
        }

参考記事:https://www.sejuku.net/blog/34465

最終的に出来上がったコードはこちら

image-preview.js
$(function() {
  $(function() {

    // プレビューHTML生成
    function buildHTML() {
      var html = `<div class="preview-box">
                    <div class="upper-box">
                      <img src="" alt="preview" class="upload-image" height="100px" width="100px">
                    </div>
                    <div class="lower-box">
                      <div class="delete-box">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`;
      return html;
    }

    $(document).on('change', '.image_upload', function() {
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      //FileReaderオブジェクトの生成
      var reader = new FileReader(); 
      //readAsDataURLで指定したFileオブジェクトを読み込む
      reader.readAsDataURL(file); 
      //読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
      reader.onload = function() {
        //直前に実行したイベントが返した値を取得する
        var image = this.result;

        // プレビュー画像がまだ場合
        if($('.preview-box').length == 0){
          // プレビュー用のhtmlを追加
          var html = buildHTML();      
          $('.icon').before(html);

          //画像を追加
          $(`.upper-box img`).attr('src', `${image}`);

        // すでにプレビュー画像が存在する場合
        }else{
          // プレビュー画像を削除
        $('.preview-box').remove();

        // プレビュー用のhtmlを追加
        var html = buildHTML();      
        $('.icon').before(html);

        //画像を追加
        $(`.upper-box img`).attr('src', `${image}`);
        }

      }
    })

    // 「削除」を押すと削除イベントが発火する
    $(document).on("click", '.delete-box', function(){
      // プレビュー画像を削除
      $('.preview-box').remove();
      // inputタグに入ってる画像ファイルも削除
      $('.image_upload').val("");
    })

  });
});

結局何をしたのか?

  • プレビュー画像の表示ができた。
  • プレビュー画像のサイズを小さくすることができた。
  • プレビュー画像を削除できるようにした。
  • 連続でファイルを選択したら、直前に選択したファイルのプレビューが消えて、新たにプレビューが表示されるようにした。

おまけ

各投稿の画像を大きくしたい。

before

スクリーンショット 2020-08-20 午後1.41.34.png

after

スクリーンショット 2020-08-20 午後1.51.56.png

_post.html.haml
- if post.image.present?
  = image_tag post.image.url, class: 'post-image', width: '300px', height: '200px'

各投稿は部分テンプレートで上記のHTMLで設定しており、image_tagのところにwidthとheightを追加して設定しました。

ただここで問題が。。。
見てわかるとおり、画像が粗くなってしまいました。

これについては解決したらまた別の記事で書きたいと思います。

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

Railsで複数のSass(.scssファイル)を使用する際の読み込み方法

はじめに

RailsでSassを使用し、かつファイルを用途・適応範囲に応じて複数に分割したとき、どのような構成、読み込み方式にするかを実際の失敗談交えながら、記載したいと思います。
最初に「つまづいたこと」で、私が体験した発生した事象や原因などを書いていますが、必要なければ読み飛ばしてもらえればと思います。

環境

ruby: 2.6.4
rails: Rails 6.0.3.1 (sass-rails gemを使用)

まず結論

Railsで複数のSassを使用する際には、読み込みをSprocketsディレクティブ方式(require_tree)ではなく、@importで読み込むこと

つまづいたこと

携わったプロジェクトではSassを使用しており、かつスタイルファイルをメンテしやすくするため、コントローラ単位で.scssファイルを作成していた。
stylesheets直下のapplication.scssで全体に共通のスタイルを設定し、stylesheets/main配下に複数の.scssファイルを配置。

application.scssでは、以下の内容を記載していた。
1. ディレクティブ方式でmain配下のscssファイルを読み込み
2. その後bootstrapなどを読み込み(@import )
3. 最後に共通スタイルを設定。

application.scss
 中略 
 *= require_tree ./main 
 *= require_self     *= require_self
 */  

@import "bootstrap/scss/bootstrap";

 中略 
// 共通スタイル
body {  
  font-family: ....
}
 中略 

やろうとしたこと

今回、共通で使用する変数 (ex $main-color: #3D85C6;)を作成して、各.scssファイルで使い回そうと思い、以下のように共通変数の定義を追記した。

application.scss(改修後)
〜 中略 〜
 *= require_tree ./main 
 *= require_self     *= require_self
 */  

@import "bootstrap/scss/bootstrap";

〜 中略 〜

// 共通変数
$main-color: #3D85C6;   

// 共通スタイル
body {  
  font-family: ....
}
〜 中略 〜

ただ、この状態だとmain配下のscssファイルで変数を使用するとエラーになったため、変数の読み込みが必要だと思い、main配下のscssファイル側でapplication.scss@importした(今思えば、なぜこんな書き方をしたのか、後悔しかない・・・)
結果、main配下のscssファイルでも、変数を使用でき、スタイルも適用されてめでたしめでたしと思っていました。

その後、起こったこと

カスタマイズを進め、applicaiton.scssに変更を加えていく中で、不可解な現象が、、、

  • ブラウザの検証機能でスタイルを見ると、同じスタイルが2つある
  • application.scss内のとあるクラスのスタイルから、一部プロパティを削除しても反映されない (前述のとおり同一スタイルが2つ存在し、片方しかプロパティ削除が反映されていない)

スクリーンショット 2020-08-17 11.45.38.png

原因

すでにお気づきかもしれませんが、「やろうとしたこと」の中で、main配下のscssファイルで、application.scssをimportしてしまったたため、
そこに記載したスタイルが二重で読み込まれてしまいました。なおかつapplication.scssでimportしたbootstrap関連のscssも二重で読み込まれてました。
ただ、applicaiton.scss内のスタイルを変更しても、反映されない原因は結局わからず・・

というわけで、main配下のscssでのimportを削除するとともに、railsで複数のscssファイルを利用するためには、どのような読み込み方法良いのか調べることになりました。。

railsで複数のscssファイルを利用際の適切な読み込み方法

調査した結果

railsguide, rails-sass(gem)のページに記載の通り、railsで複数のSass(.scssファイル)を使用する際には、Sprocketsディレクティブで読み込まずにSass@importルールを使用する必要があります。

ディレクティブ方式だと、宣言した変数やmixinが他のscssファイルから利用できない。(まさしく今回私が体験した内容)

Sassファイルを複数使用しているのであれば、Sprocketsディレクティブで読み込まずにSass @importルールを使用する必要があります。このような場合にSprocketsディレクティブを使用してしまうと、Sassファイルが自分自身のスコープに置かれるため、その中で定義されている変数やミックスインが他のSassから利用できなくなってしまいます。

Important Note
Sprockets provides some directives that are placed inside of comments called require, require_tree, and require_self. DO NOT USE THEM IN YOUR SASS/SCSS FILES. They are very primitive and do not work well with Sass files. Instead, use Sass's native @import directive which sass-rails has customized to integrate with the conventions of your Rails projects.

最終的なapplicaiton.scssの記載内容

最終的には以下のような記述内容にしました。
これにより、common.scssで定義した変数が他のmain配下のscssファイルでも読み込むことができました。

  • applicaiton.scssから、reuire_tree, require_selfの記載を削除
  • 元々applicaiton.scssに記載していた共通スタイルと変数は、common.scssを新たに作成して移管
  • @importでcommon(共通スタイル)、main配下のスタイルを順に読み込み
application.scss(最終)
〜 中略 〜
 *      
 */

@import "bootstrap/scss/bootstrap";

@import "common";
@import "main/*";

参考文献

あるべきscssファイルの取り込み方法を理解するために大変参考になりました。

rails/sass-rails

アセットパイプライン - Railsガイド

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

Railsでワクワク個人開発  第4回: ネストされたリソースでCRUD処理

ルーティングの設定

 わからない言葉が増えてきた。リソースというのは操作可能(作成・削除・更新・編集)なデータ群のことであり、ネストとは親子構造のことだった。私はUserとPlantをネストで扱いたかった。URLで考えると以下のようなイメージだ。

/users/(ユーザーid) /plants/ (ユーザーが所持しているplantのid)

 これを実現するには第3回で書いたように、モデルに対して関連付けをする必要がある。また、ルーティングに対してもいろいろやらなくてはいけない。

config/routes.rb

Rails.application.routes.draw do
  root to: 'users#index'
  devise_for :users
  resources :users do
    resources :plants
  end
end

 root to: 'users#index' とはトップページの設定、 devise_for :users はログイン機能に関するものだ。ここでは詳しく述べない。resources... が大事だ。このように入れ子構造にすることでネストされたリソースが実現できる。

記述が終わったらターミナルで rails routesを打ち込んで、どのようなルーティングがなされているか確認して欲しい。

Userのコントローラーの追記とViewの作成

 次はUserのコントローラーに追記をする。すでに以前の記事ですでにコントローラーはつくられているはずだ。以下のようにする。

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def index
    @users = User.all
  end

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

 ここではindexアクションとshowアクションのみ作る。

次にviewファイルをつくろう。index.html.erbとshow.html.erbだ。

app/views/users/index.html.erb

<h1>ユーザー一覧</h1>
<% @users.each do |user| %>
<p> アドレス <%= link_to user.email, user %></p>
<% end %>

app/views/users/show.html.erb

<h3>メールアドレス</h3>
<%= @user.email %><br>
<h3> 所有している植物 </h3>
<% @user.plants.each do |p| %>
  <li><%= p.plant_name %></li>
<% end %>

<%= link_to user.email, user %> によってindex.html.erbから ユーザーごとの詳細ページへ移動するリンクが貼られる。詳細ページでは p.plant_name によってユーザーが所有しているplantの名前が表示される。

Plantコントローラの追記とViewの作成

 今度はPlantコントローラーも書いていこう。index,newアクションなどを一気に書きくだす。(このあたりも後々解説していきたい)

class PlantsController < ApplicationController
  def index
    @user = User.find(params[:user_id])
    @plants = @user.plants
  end

  def new
    @user = User.find(params[:user_id])
    @plant = Plant.new
  end

  def create
    @plant = Plant.new(plant_params)
    current_user.plants.create(plant_params)    
  end

  def show
    @user = User.find(params[:user_id])
    @plant = @user.plants.find(params[:id])
  end

  private
  def plant_params
    params.require(:plant).permit(:user_id, :plant_name, :img)
  end

end

Viewファイルだが、今のところ使っているのはplants/create.html.erb ,plants/show.html.erb , plants/new.html.erbだけだ。 plants/index.html.erbは存在はしているが、通常のリンクではたどれない。

User一覧 > User詳細 > Plants詳細

という順序で閲覧するためだ。

plants/create.html.erb

<h1>登録完了</h1>
<%= link_to 'マイページへ', user_path(current_user.id) %>

plants/show.html.erb

<h1>植物 詳細</h1>
<h3>植物の名前</h3>
<%= @plant.plant_name %><br>
<h3> 植物の写真 </h3>
<%= @plant.img %><br>

plants/new.html.erb

<h1>植物-新規登録</h1>
<%= form_with model: [@user, @plant], local: true do |f| %>
  <p>植物名:<%= f.text_field :plant_name %></p>
  <%= f.submit "登録" %>
<% end %>

ネストされているリンク先の指定

 自分が一番悩んだのがこれだ。以下の三つ。

  • User詳細ページからPlant詳細ページへのリンク
  • User詳細ページからPlantの新規登録ページへのリンク
  • Plant新規登録ページからcreateアクション実行へのリンク

 順に紹介する。

User詳細ページからPlant詳細ページへのリンク

<!-- 略 -->
<% @user.plants.each do |p| %>
  <li><%= link_to p.plant_name ,[@user,p]%></li>
<% end %>
<!-- 略 -->

このようにlink_toのリンク先を配列にしてやることでネストされたページへリンクを貼ることができる。

以下のページが参考になった。

Railsのルーティング

User詳細ページからPlantの新規登録ページへのリンク

<!-- 略 -->
 <%= link_to '植物登録' ,new_user_plant_path(current_user.id) %>
<!-- 略 -->

current_user.idを利用することで、現在ログインしているUserのidを参照できる。これでリンクが貼れる。

Plant新規登録ページからcreateアクション実行へのリンク

<%= form_with model: [@user, @plant], local: true do |f| %>
  <p>植物名:<%= f.text_field :plant_name %></p>
  <%= f.submit "登録" %>
<% end %>

このform_with記法のリンク先もさきのlink_toと似ている。 今回の場合は [@user, @plant]とすることでネストされたリンクについてcreateアクションを行うことができる。
(この表現の仕方があっているのか…?)

おわりに

 現状のことがらはひとまず書き終えることができた。まだ、他人のIDのCRUD処理ができてしまうなど、問題は多い。間違いを質しながら個人開発をすすめていこう。

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

RailsでGraphQL APIからデータを取得・表示する

はじめに

初めまして、hinokageと申します。
Qiitaへの記事投稿初めて且つ、
プログラミングスクールを卒業したての知識で本記事を書きましたので
お手柔らかにお願いいたします。

経緯

「Annict」という、有志が開発しているアニメ関連の情報が網羅してあるDBのGraphQLAPIが公開されています。
Railsでアプリ制作をしている際、制作していたアプリ内でこれを利用するためRailsからGraphQLAPIを叩く方法を調べたところ、「Railsで制作したアプリをAPIにする方法としてGraphQLを使用する」という内容の記事は結構な数ヒットするものの、「Railsアプリ内から公開されているGraphQLAPIを叩いてデータ取得する」という内容の記事が全くヒットせず、実装に非常に苦しみました。
私と同じように「有志が制作したGraphQLAPIをRailsで利用したい!」という方に向けて少しでも役に立てるよう本記事を投稿いたします。

開発環境

  • Ruby(2.6.5)
  • Ruby on Rails(5.2.3)

導入

まず最初にGraphQLをrailsで使えるように
Ruby製のGraphQL Client 「graphql-client」を導入します。
(公式リファレンス:https://github.com/github/graphql-client

Gemfile
gem 'graphql-client'(追記)

gemfileに追記後、bundle install

bundle install

次に、使用したいAPIのアクセストークンの作成を行います。
ここでは例としてGithubのGraphQL APIを使用します。

GitHub APIのアクセストークンは
GitHubの自分のアイコンをクリック
Settings > Developer settings > Personal access tokens >Generate new token
から作成できます。
(ここでは詳しい作成方法は省略させていただきます。)

httpアダプタ、Schema、クライアント設定のためapplication.rbに以下を追記します。
(今回はgql_testというアプリ名で制作しています。)

config/application.rb
module GqlTest(ご自身のアプリ名)
  ...

  AUTH_HEADER = "Bearer xxx"
 # xxx=作成した際に表示されたアクセストークン
  HTTP = GraphQL::Client::HTTP.new("https://api.github.com/graphql") do 
  #上記には、接続したいAPIのエンドポイントURLを記入
    def headers(context)
      { "Authorization": AUTH_HEADER }
    end
  end
  Schema = GraphQL::Client.load_schema(HTTP)
  # 上記を使って API サーバーから GraphQL Schema 情報を取得
  Client = GraphQL::Client.new(schema: Schema, execute: HTTP)
  # 上記を使ってクライアントを作成
end

これで大まかな設定は完了です。

クエリ作成

次に取得したいデータのクエリを書きます。
ちなみに

クエリとは
データベース管理システムに対する問合せ(処理要求)のこと。

簡単に書くと
「欲しいデータを取得するためのコード」
です。

GraphQLのクエリの書き方は
https://employment.en-japan.com/engineerhub/entry/2018/12/26/103000
上記のサイトが参考になると思います。もし自分でクエリを書きたい場合はご活用ください。

今回はGitHubAPIを使用するのでクエリは以下のようなものを使用しました。
自分のリポジトリの新しいものから5つ分取得するクエリとなっています。

query {
    viewer {
      repositories(last:5) {
        edges {
          node {
            name
          }
        }
      }
    }
  }

また、自分でクエリを書きたい場合はAPIに応じたSchemaを知る必要があります。
ちなみに

Schemaとは
GraphQL APIの仕様を表現するものです。スキーマ定義言語(SDL(Schema Definition Language))を使って表現します。

先程のクエリを例にすると「viewer」「repositories」「node」「name」など全てSchemaです。
API作成時、作成者がデータを取り出す方法をSchemaとして文字に定義しており、
それをAPI使用者がSchemaを用いてデータを取得するために書くコードがクエリだと考えてもらえば
わかりやすいかもしれません。

「クエリがどういうものなのか」を分かりやすく伝えるための自分の中で辿り着いた例えが
辞典でいう目次みたいなものがSchemaです。

例えば、辞典でとある動物を調べたい、
しかしその動物の〇〇類〇〇科〇〇目などは分かるが名前はわからないみたいな時に
目次のページを見ると思います。この目次のページがSchemaです。
目次を見て〇〇類のページを開きます。
またその辞典には〇〇類の最初のページに「〇〇科一覧」みたいな目次があるかもしれません。
これもSchemaです。

つまりはSchemaは
目的のデータに辿り着きたい時の検索手段
みたいなものだと考えれば分かりやすいかもしれません。
その検索手段をAPI作成者側が文字に定義しているのが
schemaだと私は解釈して使用しています。
(間違っていたら申し訳ないので、ぜひ一度GraphQL Schemaとググって調べてください)

さて、Schemaの種類や使い方については
各APIの公式リファレンスを読めば書いてあると思いますが
私は「GraphiQL」というアプリケーションを使用して調べることが多いです。
スクリーンショット 2020-08-17 18.27.53.png

GraphiQLはSchemaの確認や検索ができるほか、
APIへの接続やクエリ、返ってくるデータの確認ができるため非常に便利なツールとなっています。

このアプリはブラウザ上で使用することもできますが
オススメはスタンドアローンで使用できるダウンロード版です。
https://electronjs.org/apps/graphiql

少し話が逸れてしまいました本題に戻ります。

実装

先ほどのクエリをcontorollerに追加します。
その際、下記のように記述します。

controllers/blogs_controller.rb
class BlogsController < ApplicationController
  Query = GqlTest::Client.parse <<-GRAPHQL
  query {
    viewer {
      repositories(last:5) {
        edges {
          node {
            name
          }
        }
      }
    }
  }
    GRAPHQL

  def index
    @works = result
  end

  private
  def result
    response = GqlTest::Client.query(Query)
  end
end

privateメソッドの下層に
resultというメソッドを用意して
クエリの結果データをrailsで使用できるように加工しています。
そして、使用したいアクション内でresultを用いることで、
結果データを使用することができます。

@worksというインスタンス変数にAPIからの結果データを格納したので
これをviewで使用してみます。最新5件のリポジトリ名が表示されればOKです。

index.html.erb
<h2>私が最近作成したリポジトリは以下の5つです</h2>
<%= @works%>

スクリーンショット 2020-08-19 22.43.01.png

あれ?オブジェクトしか表示されない…?

実はこの時点では取り出したいデータを指定していなかったので
オブジェクトが表示されるだけになっています。
これは以下のようにすると表示することができます。

controllers/blogs_controller.rb
  def index
    @works = result.data.viewer.repositories.edges
  end

private
  def result
    response = GqlTest::Client.query(Query)
  end

これだけだと、変数内に配列のようにリポジトリ名が複数入っている状態なので、
下記のように表示されてしまいます。
スクリーンショット 2020-08-19 22.42.21.png

そのため、viewファイルの方でeachメソッドを使い1つずつ表示するようにします。

index.html.erb
<% @works.each do |work|%>
  <p><%= work.node.name%></p>
<% end %>

こうすることで表示することができました。
スクリーンショット 2020-08-19 22.51.53.png

…しかしこれだと、追記したコードの意味が分からないですよね。

コードの解説

実は取得したデータだけでは目的の
「自分の最新リポジトリ5件のタイトル」というデータ以外のものも含まれるので、
クエリで書いたように何の情報が欲しいのかを記述する必要があります。

つまり、「自分の最新リポジトリ5件のタイトルを取得する」というクエリを書いても、
結果として返ってくるデータは「自分の最新リポジトリ5件のタイトル」だけでは無いということです。

GraphiQLを使って、先程と同じクエリを送った時にどのようなデータが返ってくるのか見ると非常にわかりやすいです。

スクリーンショット 2020-08-19 20.01.33.png

画面左側のコードが先程と同じ「最新リポジトリ5件のタイトルを取得する」クエリです
画面右側はそのクエリをAPIに送った時に返ってきた結果データです。
スクリーンショット 2020-08-19 23.59.14.png

結果データを見るとクエリで書いたSchemaに値が入って返ってきてるのが分かると思います。
また、コードの書かれ方がハッシュと同じような形でkeyとvalueが含まれているのが分かると思います。

この結果データがハッシュであった場合、リポジトリのタイトルであるnameの値を取り出そうとすると
resultという変数に結果データが格納されていた時

result[:data][:viewer][:repositories][:edges].each do |work|
 work[:node][:name]
end

このようなコードを書くと思います。
結果データはハッシュでは無いため、このような取り出し方はできないのですが
(ちなみに結果データはJSON形式で返ってきているようです。)
公式リファレンスを読むとハッシュでkeyになっていたものをメソッドのように扱うことで
上記で書いたハッシュの展開と似たような方法で取り出すことができるようです。
つまり、コードは

controllers/blogs_controller.rb
 def index
   @works = result.data.viewer.repositories.edges
 end

private
 def result
   response = GqlTest::Client.query(Query)
 end
index.html.erb
<% @works.each do |work|%>
  <p><%= work.node.name%></p>
<% end %>

と書くことで目的のデータが表示できるようになります。

(夜中に殴り書きしたので色々誤字脱字あるかもしれません…)

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

【Rails 6.0】Action Cable を解説しながらプログレスバーを実装する

TL;DR

  • Action Cable を使用してプッシュ通信のプログレスバーを実装した
  • Action Cable について解説する
  • ProgressChannel.broadcast_to(current_user, percent: (index+1) * 100 / pictures.length)

背景

旅行の思い出投稿サイトを作ったときに写真アップロードが遅くて、UX 的にアップロードの進捗状況を表すプログレスバーがあったほうが良いのではないかとなり、Action Cable の勉強も兼ねて実装しました

モノ

ezgif-2-3851e5ddc0e8.gif

Action Cable 解説

Rails に Action Cable という WebSocket と Rails のその他の部分をシームレスに統合するための機能が存在します
Rails 公式なので Rails ガイドを見るのが良いと思ったのですが、Rails ガイドの解説がとても分かりづらかったので自分でまとめ直してみました
参考になれば幸いです

図が一番わかり易い

Rails ガイドがわかりづらかった理由の一つが図がなかったことだと思ったので図を作りました
どこか間違ってたら教えてください
スクリーンショット 2020-08-20 0.18.22.png

用語解説

スクリーンショット 2020-08-20 0.21.08.png

Rails ガイドと順番が違うかもしれませんが、一番わかり易いと思う順番にしました

究極的にはパブリッシャからサブスクライバへ通信が行われる pub/sub リンクです
Rails アプリケーションの controller でブロードキャストを行い、チャネルを通して各タブ(サブスクライバ)に配信します

プログレスバーの実装

プログレスバーを主題に実際の実装を見ていきます

実行環境

  • ruby:2.6.5 (Docker image)
  • Rails 6.0.2

まずは rails g channel <チャネル名> でひな形を作成します

$ rails g channel progress

サーバー側

チャネル

サーバー側のプログレスチャネルです

app/channels/progress_channel.rb
class ProgressChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

subscribedunsubscribedのひな形は用意されていて、それぞれサブスクリプションができた/なくなったときの挙動を書きます
一般的には、stream_from "progress_#{params[:progress_id]}" のような形で受け取るブロードキャストを指定します
しかし、ここでは stream_for を使用し、ユーザーごとに変わるブロードキャストを指定しています
これで、progress:Z2lkOi8vbWVtb3BpYy9Vc2VyLzE (ブロードキャスト名) のようなブロードキャストを受信することができます

コネクション

サーバー側ではこのコネクションクラスをもとに、コネクションインスタンスを作成します

app/channels/application_cable/progress_channel.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private
      def find_verified_user
        User.find(session['warden.user.user.key'][0][0])
      rescue
        reject_unauthorized_connection
      end

      def session
        @session ||= cookies.encrypted[Rails.application.config.session_options[:key]]
      end
  end
end

identified_by に指定したものを参考に、サーバーはコネクションを識別できるようになります
このコードでは、current_user に入る User オブジェクトをもとに識別しています
アプリケーションで Devise を使用しているので、ここの実装は
https://qiita.com/masuidrive/items/fd13ab8d9410206a1b91
を参考にしました

パブリッシャ

今回は model のところにブロードキャストするコードを書きます

app/models/album_form.rb
pictures.each_with_index do |picture, index|
   album.pictures.new(picture_name: picture)
   ProgressChannel.broadcast_to(
      user,
      percent: (index+1) * 100 / pictures.length
   )
end

ここでの usercurrent_user に当たります(実装上、変数名が違います)
ここでクライアントにわたす data (percent: (index+1) * 100 / pictures.length) を指定してプログレスバーの進めるわけです
単純に枚数で進捗の%を計算しています
このブロードキャストがチャネルの stream_for current_user につながり、current_user がいるクライアントにデータが送信されます

以上で、サーバー側の実装は終わりです

クライアント側

好きな場所にプログレスバーを置きます

app/views/albums/index.html.erb
<div class="progress-percentage text-xs-center">0%</div>
<progress class="progress progress-striped" max="100" value="0"></progress>

クライアント側でもコネクションを作成しないといけません

app/javascript/channles/consumer.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()
app/javascript/channels/progress_channel.js
import consumer from "./consumer"

consumer.subscriptions.create("ProgressChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
    $('.progress-percentage').text(`${data.percent}%`)
    $('progress').prop('value', data.percent)
  }
});

consumer.jsでコンシューマを作成し、progress_channel.js でコネクション(サブスクリプション)を作成しています
今回は ProgressChannel につながるサブスクリプションです
connected()にコネクションができたときの動作、disconnected()にコネクションがなくなったときの動作を記述します
received()にはブロードキャストされたデータを受け取ったときの挙動を書きます
プログレスバーの進捗状況がサブスクライバにブロードキャストされて受け取るので、ここでプログレスバーの各要素を jQuery で書き換えます

完成!

まとめ

以上のコードを実装することでユーザーごとにプッシュ通信にするプログレスバーが実装できます
Qiita 初投稿で ActionCable も学びたてなので間違っているところもあるかもしれません、その部分はご指摘・ご教授いただけるとありがたいです
また、記事のこの部分がわかりづらいというコメントもお待ちしています!

参考記事

https://railsguides.jp/action_cable_overview.html
https://qiita.com/QUANON/items/0ba35cd0eb5d720f4237
https://qiita.com/masuidrive/items/fd13ab8d9410206a1b91

P.S.

memopic』 という旅行の思い出・アルバム・写真共有サービスを作っています。是非使ってみてください!
ご意見・ご要望などあれば @memopic_bambooまで DM お願いします!

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

【Rails 6.0】Action Cable を解説しながらプログレスバーを実装する

TL;DR

  • Action Cable を使用してプッシュ通信のプログレスバーを実装した
  • Action Cable について解説する
  • ProgressChannel.broadcast_to(current_user, percent: (index+1) * 100 / pictures.length)

背景

旅行の思い出投稿サイトを作ったときに写真アップロードが遅くて、UX 的にアップロードの進捗状況を表すプログレスバーがあったほうが良いのではないかとなり、Action Cable の勉強も兼ねて実装しました

モノ

ezgif-2-3851e5ddc0e8.gif

Action Cable 解説

Rails に Action Cable という WebSocket と Rails のその他の部分をシームレスに統合するための機能が存在します
Rails 公式なので Rails ガイドを見るのが良いと思ったのですが、Rails ガイドの解説がとても分かりづらかったので自分でまとめ直してみました
参考になれば幸いです

図が一番わかり易い

Rails ガイドがわかりづらかった理由の一つが図がなかったことだと思ったので図を作りました
どこか間違ってたら教えてください
スクリーンショット 2020-08-20 0.18.22.png

用語解説

スクリーンショット 2020-08-20 0.21.08.png

Rails ガイドと順番が違うかもしれませんが、一番わかり易いと思う順番にしました

究極的にはパブリッシャからサブスクライバへ通信が行われる pub/sub リンクです
Rails アプリケーションの controller でブロードキャストを行い、チャネルを通して各タブ(サブスクライバ)に配信します

プログレスバーの実装

プログレスバーを主題に実際の実装を見ていきます

実行環境

  • ruby:2.6.5 (Docker image)
  • Rails 6.0.2

まずは rails g channel <チャネル名> でひな形を作成します

$ rails g channel progress

サーバー側

チャネル

サーバー側のプログレスチャネルです

app/channels/progress_channel.rb
class ProgressChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

subscribedunsubscribedのひな形は用意されていて、それぞれサブスクリプションができた/なくなったときの挙動を書きます
一般的には、stream_from "progress_#{params[:progress_id]}" のような形で受け取るブロードキャストを指定します
しかし、ここでは stream_for を使用し、ユーザーごとに変わるブロードキャストを指定しています
これで、progress:Z2lkOi8vbWVtb3BpYy9Vc2VyLzE (ブロードキャスト名) のようなブロードキャストを受信することができます

コネクション

サーバー側ではこのコネクションクラスをもとに、コネクションインスタンスを作成します

app/channels/application_cable/progress_channel.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private
      def find_verified_user
        User.find(session['warden.user.user.key'][0][0])
      rescue
        reject_unauthorized_connection
      end

      def session
        @session ||= cookies.encrypted[Rails.application.config.session_options[:key]]
      end
  end
end

identified_by に指定したものを参考に、サーバーはコネクションを識別できるようになります
このコードでは、current_user に入る User オブジェクトをもとに識別しています
アプリケーションで Devise を使用しているので、ここの実装は
https://qiita.com/masuidrive/items/fd13ab8d9410206a1b91
を参考にしました

パブリッシャ

今回は model のところにブロードキャストするコードを書きます

app/models/album_form.rb
pictures.each_with_index do |picture, index|
   album.pictures.new(picture_name: picture)
   ProgressChannel.broadcast_to(
      user,
      percent: (index+1) * 100 / pictures.length
   )
end

ここでの usercurrent_user に当たります(実装上、変数名が違います)
ここでクライアントにわたす data (percent: (index+1) * 100 / pictures.length) を指定してプログレスバーの進めるわけです
単純に枚数で進捗の%を計算しています
このブロードキャストがチャネルの stream_for current_user につながり、current_user がいるクライアントにデータが送信されます

以上で、サーバー側の実装は終わりです

クライアント側

好きな場所にプログレスバーを置きます

app/views/albums/index.html.erb
<div class="progress-percentage text-xs-center">0%</div>
<progress class="progress progress-striped" max="100" value="0"></progress>

クライアント側でもコネクションを作成しないといけません

app/javascript/channles/consumer.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()
app/javascript/channels/progress_channel.js
import consumer from "./consumer"

consumer.subscriptions.create("ProgressChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
    $('.progress-percentage').text(`${data.percent}%`)
    $('progress').prop('value', data.percent)
  }
});

consumer.jsでコンシューマを作成し、progress_channel.js でコネクション(サブスクリプション)を作成しています
今回は ProgressChannel につながるサブスクリプションです
connected()にコネクションができたときの動作、disconnected()にコネクションがなくなったときの動作を記述します
received()にはブロードキャストされたデータを受け取ったときの挙動を書きます
プログレスバーの進捗状況がサブスクライバにブロードキャストされて受け取るので、ここでプログレスバーの各要素を jQuery で書き換えます

完成!

まとめ

以上のコードを実装することでユーザーごとにプッシュ通信にするプログレスバーが実装できます
学びたてなので間違っているところもあるかもしれません、その部分はご指摘・ご教授いただけるとありがたいです
また、記事のこの部分がわかりづらいというコメントもお待ちしています!

参考記事

https://railsguides.jp/action_cable_overview.html
https://qiita.com/QUANON/items/0ba35cd0eb5d720f4237
https://qiita.com/masuidrive/items/fd13ab8d9410206a1b91

P.S.

『memopic』 という旅行の思い出・アルバム・写真共有サービスを作っています。是非使ってみてください!
ご意見・ご要望などあれば @memopic_bambooまで DM お願いします!

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

商品出品機能のコード全文

フリマアプリの商品出品機能

最終課題で実装した商品出品ページの全コードを記録用に書きます!

機能

・ancestry:多階層カテゴリー
・ActiveStorage:画像投稿
・jp_prefecture:都道府県を扱うGem
・active_hash:静的データ作成

Active Storageのセットアップ

下記をみて導入方法を確認する
Rails ガイド

ルーティング

routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
  }
  devise_scope :user do
    get 'sending_destinations', to: 'users/registrations#newSendingDestination'
    post 'sending_destinations', to: 'users/registrations#createSendingDestination'
  end
  root "items#index"

  resources :items, only: [:new, :create, :update] do
    collection do
      get 'get_category_child', to: 'items#get_category_child', defaults: { format: 'json' }
      get 'get_category_grandchild', to: 'items#get_category_grandchild', defaults: { format: 'json' }
    end
  end
end

コントローラー

items_controller
class ItemsController < ApplicationController

  def index
    @items_category = Item.where("buyer_id IS NULL AND trading_status = 0 AND category_id < 200").order(created_at: "DESC")
    @items_brand = Item.where("buyer_id IS NULL AND  trading_status = 0 AND brand_id = 1").order(created_at: "DESC")
  end

  def new
    @item = Item.new
    @item.item_imgs.new
    @category_parent = Category.where(ancestry: nil)
      # 親カテゴリーが選択された後に動くアクション
    def get_category_child
      @category_child = Category.find("#{params[:parent_id]}").children
      render json: @category_child
      #親カテゴリーに紐付く子カテゴリーを取得
    end
      # 子カテゴリーが選択された後に動くアクション
    def get_category_grandchild
      @category_grandchild = Category.find("#{params[:child_id]}").children
      render json: @category_grandchild
      #子カテゴリーに紐付く孫カテゴリーの配列を取得
    end
  end

  def create
    @item = Item.new(item_params)
    unless @item.valid?
      @item.item_imgs.new
      render :new and return
    end

    @item.save
    redirect_to root_path
  end


  private

  def item_params
    params.require(:item).permit(:name, :introduction, :price, :prefecture_code, :brand_id, :pref_id, :size_id, :item_condition_id, :postage_payer_id, :preparation_day_id, :postage_type_id, :category_id, :trading_status, item_imgs_attributes: [:url, :id]).merge(seller_id: current_user.id)
  end

Haml

sell.html.haml
.sell-container
  = form_for @item do |f|
    -# 画像部分
    .sell-container__content
      .sell-title
        %h3.sell-title__text
          出品画像
          %span.sell-title__require
            必須
      .sell-container__content__box
        %ul.output-box
          %div#image-input
            = f.fields_for :item_imgs do |image|
              = image.label :url, id: 'image-input__label' do
                = image.file_field :url, accept: "image/*", class: "js-file", data: {index: image.index}, style: 'display: none;'
                %pre
                  %i.fas.fa-camera.fa-lg
      .error-messages
        %p
          =@item.errors.messages[:item_imgs][0]
    -# 商品概要部分
    .sell-container__content
      .sell-title
        %h3.sell-title__text
          商品名
          %span.sell-title__require
            必須
      = f.text_field :name, {class: 'sell-container__content__name', maxlength: '40', placeholder: '商品名(必須 40文字まで)'}
      .error-messages
        %p
          =@item.errors.messages[:name][0]

      .sell-title
        %h3.sell-title__text
          商品の説明
          %span.sell-title__require
            必須
      = f.text_area :introduction,{class: 'sell-container__content__description', rows: '7', maxlength: '1000', placeholder: '商品説明'}
      .sell-container__content__word-count
        %p.error-messages
          =@item.errors.messages[:introduction][0]
        %span#word-count
          0&#47;1000

    -# 詳細部分
    .sell-container__content
      %h3.sell-sub-head 商品の詳細
      .sell-container__content__details
        .sell-title
          %h3.sell-title__text
            カテゴリー
            %span.sell-title__require
              必須
        .sell-collection_select.category
          .select_collection_select-category
            = f.collection_select :category_id, @category_parent,:id, :name, {prompt: "--"},{class: 'sell-collection_select__label', id:'parent_category'} 
        .error-messages
          %p
            =@item.errors.messages[:category_id][0]

        .sell-title
          %h3.sell-title__text
            サイズ
            %span.sell-title__require
              必須
        .sell-collection_select
          = f.collection_select :size_id, Size.all, :id, :value, {prompt: "--"} ,{class: 'sell-collection_select__label'}
        .error-messages
          %p
            =@item.errors.messages[:size_id][0]

        .sell-title
          %h3.sell-title__text
            ブランド
            %span.sell-title__require.arbitrary
              任意
        .sell-collection_select
          = f.collection_select :brand_id, Brand.all, :id, :name, {prompt: "--"} ,{class: 'sell-collection_select__label'}
        .error-messages

        .sell-title
          %h3.sell-title__text
            商品の状態
            %span.sell-title__require
              必須
        .sell-collection_select
          = f.collection_select :item_condition_id, ItemCondition.all, :id, :name, {prompt: "--"}, {class: 'sell-collection_select__label'}
        .error-messages
          %p
            =@item.errors.messages[:item_condition_id][0]

    -# 配送部分
    .sell-container__content
      %h3.sell-sub-head
        %p 配送について
      .sell-container__content__details
        .sell-title
          %h3.sell-title__text
            配送方法
            %span.sell-title__require
              必須
        .sell-collection_select
          = f.collection_select :postage_type_id, PostageType.all, :id, :name, {prompt: "--"}, {class: 'sell-collection_select__label'}
        .error-messages
          %p
            =@item.errors.messages[:postage_type_id][0]

        .sell-title
          %h3.sell-title__text
            配送料の負担
            %span.sell-title__require
              必須
        .sell-collection_select
          = f.collection_select :postage_payer_id, PostagePayer.all.map, :id, :name, {prompt: "--"}, {class: 'sell-collection_select__label'}
        .error-messages
          %p
            =@item.errors.messages[:postage_payer_id][0]

        .sell-title
          %h3.sell-title__text
            発送元の地域
            %span.sell-title__require
              必須
        .sell-collection_select
          = f.collection_select :prefecture_code, JpPrefecture::Prefecture.all, :code, :name, {include_blank: '--'}, class: 'sell-collection_select__label'
        .error-messages
          %p
            =@item.errors.messages[:prefecture_code][0]

        .sell-title
          %h3.sell-title__text
            発送までの日数
            %span.sell-title__require
              必須
        .sell-collection_select
          = f.collection_select :preparation_day_id, PreparationDay.all, :id, :value, {prompt: "--"}, {class: 'sell-collection_select__label'}
        .error-messages
          %p
            =@item.errors.messages[:preparation_day_id][0]
    -# 価格部分
    .sell-container__content
      %h3.sell-sub-head
        %p 販売価格(3009,999,999)
      .sell-container__content__price
        .sell-title
          %h3.sell-title__text
            販売価格
            %span.sell-title__require
              必須
        .sell-container__content__price__form
          = f.label :price, class: 'sell-container__content__price__form__label' do
            ¥
            = f.number_field :price, {placeholder: '0', value: '', autocomplete:"off", class: 'sell-container__content__price__form__box'}
      .error-messages#error-price
        =@item.errors.messages[:price][0]

      .sell-container__content__commission
        .sell-container__content__commission__left
          販売手数料 10%
        .sell-container__content__commission__right 
      .sell-container__content__commission
        .sell-container__content__commission__left
          販売利益
        .sell-container__content__commission__right 

      .submit-btn
        = f.submit '出品する', class: 'submit-btn--Btn'
      .submit-btn
        = link_to 'もどる', root_path, class: 'submit-btn--Btn return'
      .attention-box
        %p
          禁止されている行為および出品物を必ずご確認ください。偽ブランド品や盗品物
          などの販売は犯罪であり、法律により処罰される可能性があります。また、出品をもちまして加盟店規約に同意したことになります。

必須項目が見入力の場合、エラーメッセージが出るように設定しています。
また、住所はjp_prefectureという都道府県を扱うGemを使用しました!

*ポイント*
ボックスはf.collectionに統一して記述する。
f.selectを使用するとvalueが文字でDBに送られる為、idが0で登録される為、後々の商品詳細ページでデータを表示させる時にエラーが出ます。

JS

items.js
$(document).on('turbolinks:load', ()=> {
  // 画像用のinputを生成する関数
  const buildFileField = (index)=> {
    const html = `<input accept="image/*" class="js-file" data-index="${index}" style="display: none;", type="file" name="item[item_imgs_attributes][${index}][url]" id="item_item_imgs_attributes_${index}_url">`;
    return html;
  }
  // プレビュー用のimgタグを生成する関数
  const buildImg = (index, url)=> {
    const html = `<img data-index="${index}" src="${url}" width="100px" height="100px">`;
    return html;
  }

  // file_fieldnameに動的なindexをつける為の配列
  let fileIndex = [1,2,3,4,5,6,7,8,9,10];

  $('#image-input').on('change', '.js-file', function(e) {
    // labelタグのfor属性を変更
    $('#image-input__label').attr('for', 'item_item_imgs_attributes_' + fileIndex[0] + '_url');
    // fileIndexの先頭の数字を使ってinputを作る
    $('#image-input').append(buildFileField(fileIndex[0]));
    fileIndex.shift();
    // 末尾の数に1足した数を追加する
    fileIndex.push(fileIndex[fileIndex.length - 1] + 1)
    const targetIndex = $(this).parent().data('index');
    // ファイルのブラウザ上でのURLを取得する
    const file = e.target.files[0];
    const blobUrl = window.URL.createObjectURL(file);
    $('#image-input').before(buildImg(targetIndex, blobUrl));
  });
});

JSでは大きく分けて2つ。
①画像のプレビュー
②Inputタグの生成

get_category_child.json.jbuilder
json.array! @category_child do |child|
  json.id child.id
  json.name child.name
end
get_category_grandchild.json.jbuilder
json.array! @category_grandchild do |grandchild|
  json.id grandchild.id
  json.name grandchild.name
end

モデルは割愛します。

今回は以上です。

間違いなどあればご指摘いただけますと幸いです^^

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

rails csv ファイル読み込み トライ

【Ruby on Rails】CSVインポート
Rails上で、CSVファイルを読み込んでみた
【Rails】CSVファイルからデータをインポート

これを参考にトライ

結果その通りにやったらできました。
(投稿者様、ありがとうございます)

だが、rubyの文法でつまづく。
だれかわかる方いらっしゃたら助けていただけるとありがたいです。

app/controllers/users_controller.rb
def index
    @users = User.all
  end

  def import
    # fileはtmpに自動で一時保存される
    User.import(params[:file])
    redirect_to users_url
  end

このimportコントローラーUser.importのimportメソッドは、user Modelを操作するために、下記で、クラスメソッドとして定義されている、importメソッドであることはわかった。

app/model/user.rb
class User < ApplicationRecord
  def self.import(file)
    CSV.foreach(file.path, headers: true) do |row|
      # IDが見つかれば、レコードを呼び出し、見つかれなければ、新しく作成
      user = find_by(id: row["id"]) || new
      # CSVからデータを取得し、設定する
      user.attributes = row.to_hash.slice(*updatable_attributes)
      # 保存する
      user.save
    end
  end

  # 更新を許可するカラムを定義
  def self.updatable_attributes
    ["id", "name", "age"]
  end
end

まず、

user = find_by(id: row["id"]) || new

でつまづいた。
調べると、
https://www.sejuku.net/blog/19044
nil判定を行っていることは理解
find_by(id: row["id"])がnilなら newをuserに代入している。

でも "new"とはなんだろう、インスタンスか?
公式document
https://docs.ruby-lang.org/ja/latest/class/CSV=3a=3aRow.html
をみると、
CSV::Rowクラスの、newメソッドなのではないかと予想
であれば、
row.newを省略して書いている??

もう一個

row.to_hash.slice(*updatable_attributes)

がわからない。

rowは、csvの特定の行
それをto_hashでハッシュにしている
sliceで ["id", "name", "age"]のみ取扱たいが、
to_hashメソッドではを使うと、["id", "name", "age"]がハッシュのキー(あるいは値)として使われるのだろうか?
https://docs.ruby-lang.org/ja/latest/class/CSV=3a=3aRow.htm

謎は深まる。

あと、*updatable_attributes
引数にアスタリスク付きのメソッドを指定している記法がわからない。

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

AWSEC2でただrails sをするとこうなってしまう!解決策は・・・

ターミナル
[ec2-user@ip-172-31-38-149 fleamarket_sample_80d]$ rails s
Usage:
  rails new APP_PATH [options]

Options:
      [--skip-namespace], [--no-skip-namespace]              # Skip namespace (affects only isolated applications)
  -r, [--ruby=PATH]                                          # Path to the Ruby binary of your choice
                                                             # Default: /home/ec2-user/.rbenv/versions/2.6.5/bin/ruby
  -m, [--template=TEMPLATE]                                  # Path to some application template (can be a filesystem path or URL)
  -d, [--database=DATABASE]                                  # Preconfigure for selected database (options: mysql/postgresql/sqlite3/oracle/frontbase/ibm_db/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc)
                                                             # Default: sqlite3
      [--skip-gemfile], [--no-skip-gemfile]                  # Don't create a Gemfile
  -G, [--skip-git], [--no-skip-git]                          # Skip .gitignore file
      [--skip-keeps], [--no-skip-keeps]                      # Skip source control .keep files
  -M, [--skip-action-mailer], [--no-skip-action-mailer]      # Skip Action Mailer files
      [--skip-action-mailbox], [--no-skip-action-mailbox]    # Skip Action Mailbox gem
      [--skip-action-text], [--no-skip-action-text]          # Skip Action Text gem
  -O, [--skip-active-record], [--no-skip-active-record]      # Skip Active Record files
      [--skip-active-storage], [--no-skip-active-storage]    # Skip Active Storage files
  -P, [--skip-puma], [--no-skip-puma]                        # Skip Puma related files
  -C, [--skip-action-cable], [--no-skip-action-cable]        # Skip Action Cable files
  -S, [--skip-sprockets], [--no-skip-sprockets]              # Skip Sprockets files
      [--skip-spring], [--no-skip-spring]                    # Don't install Spring application preloader
      [--skip-listen], [--no-skip-listen]                    # Don't generate configuration that depends on the listen gem
  -J, [--skip-javascript], [--no-skip-javascript]            # Skip JavaScript files
      [--skip-turbolinks], [--no-skip-turbolinks]            # Skip turbolinks gem
  -T, [--skip-test], [--no-skip-test]                        # Skip test files
      [--skip-system-test], [--no-skip-system-test]          # Skip system test files
      [--skip-bootsnap], [--no-skip-bootsnap]                # Skip bootsnap gem
      [--dev], [--no-dev]                                    # Setup the application with Gemfile pointing to your Rails checkout
      [--edge], [--no-edge]                                  # Setup the application with Gemfile pointing to Rails repository
      [--rc=RC]                                              # Path to file containing extra configuration options for rails command
      [--no-rc], [--no-no-rc]                                # Skip loading of extra configuration options from .railsrc file
      [--api], [--no-api]                                    # Preconfigure smaller stack for API only apps
  -B, [--skip-bundle], [--no-skip-bundle]                    # Don't run bundle install
  --webpacker, [--webpack=WEBPACK]                           # Preconfigure Webpack with a particular framework (options: react, vue, angular, elm, stimulus)
      [--skip-webpack-install], [--no-skip-webpack-install]  # Don't run Webpack install

このような表記になってしまいますが解決策は
rails s RAILS_ENV=production です。

以上。

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