20200628のRailsに関する記事は30件です。

[rspec] 特定のテストのみ実行する方法

前提

ruby 2.6.3
Rails 6.0.3.1
rspec-rails (3.9.1)


system specのテストを書いていると、テストを走らせてから実行にかなり時間がかかりますよね。

個々のテストは、テスト作成時にピンポイントで行い、全てのテストが全てパスしているかは、リモートリポジトリにpushした時にcircle CIに任せるような、割り振りにしたいと考えました。

てな感じで、個々のテストをピンポイントで走らせたい場合について調べました。

特定のファイルのみテストを走らせる

#bundle exec  rspec spec/パスを指定する
bundle exec  rspec spec/system/users_spec.rb

特定ののみテストを走らせる

#bundle exec  rspec spec/パスを指定後に、行数を指定する
bundle exec  rspec spec/system/users_spec.rb:2

model_specはまだしも、feature_specsystem_specは実行が重いので、テスト結果をすぐに知りたい場合は、ファイル or 行指定する方が時短になりそうですね。

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

Railsのnokogiriでスクレイピングしてactiverecord-importで配列をDBに保存する

スクレイピングしてDBに保存したい人向けです

外部からデータ取ってくることに憧れてたので、初めてスクレイピングやった忘備録です。
初めてスクレイピングしたのでおてやわらかにお願いします笑

何をスクレイピングしたの?

paypayのキャンペーンwebページから店舗名を取得しました!
image.png

スクレイピング前のDB

image.png

スクレイピング後のDB

image.png

環境

  • sqlite3
  • ruby 2.6.3
  • Rails 6.0.2.1
Gemfile.
gem 'activerecord-import'
gem 'nokogiri'

bundle installしといてください!

まずはスクレイピングしてみる

今回はcontrollerから呼び出したかったので、moduleとして作成します。

app/controllers/concerns/paypay_scrapes_concern.rb
module PaypayScrapesConcern
  require 'open-uri'
  require 'nokogiri'

  def set_paypay_shops
    url = 'https://paypay.ne.jp/notice/20200604/01/'

    charset = nil
    html = open(url) do |f|
      charset = f.charset
      f.read
    end

    doc = Nokogiri::HTML.parse(html, nil, charset)

    @shops = []

    doc.xpath('//div[@class="article__contents post"]').css('tr').each do |node|
      @shops << node.css("td[1]").text
    end

    @shops = @shops.drop(1)
    p @shops
  end
end

確認したい場合は、app配下直に置いて、ruby paypay_scrapes_concern.rbで確認できます。

表示結果は以下です。

["あさひ", "味千ラーメン 掛川インター店", "遠州屋", "株式会社縁 開縁ダイニング縁や", "大石農場ハム工房", "大手門うおそう", "OZ", "KAKEGAWA 1番地", "掛川グランドホテル", "華月苑", "インド・ネパール料理レストラン GANESHA", "かねきや旅館", "カレー・ザ・ロック", "喜縁旬菜 ZIKAN", "喬菜 まさ吉", "餃子と串カツ 遠州誠家", "串&Bar FuQ(ふく)", "琴菊", "ことのや", "魚処 粋", "椎の木茶屋", "真味楼", "寿し処 八幡", "タナカ", "中国料理四川", "戸塚屋", "巴屋", "ナムズ", "にんちゃんち ", "ひいらぎや", "ファニーファーム", "Food Labo 房’s", "ふらっと", "ベル・エポック", "MAX'S DINER", "まるましらすや", "麺屋 RiQ", "八咫烏", "ラーメン 男前", "らーめん若虎", "LA MAREA 1980 DAN"]

作ったモジュールをcontrollerでincludeしてviewで確認

shops_controller.rb
class ShopsController < ApplicationController

  include PaypayScrapesConcern

  def new
    if params[:format] == "paypay"
      @shops = set_paypay_shops
    end
    @shop = Shop.new
  end
end
new.html.slim
h2 お店を追加する
= render 'form'
= link_to "paypayのキャンペーンを表示する", new_shop_path("paypay")
- if @shops.present?
  - @shops.each do |shop|
    p = shop

localhost:3000/shop/new
↓↓↓↓↓↓
image.png
ボタンを押すと
localhost:3000/shop/new.paypay
↓↓↓↓↓↓
image.png

これでviewで確認できました。

保存用のactionを作成

まずはroutesにactionを追加

routes.rb
resources :shops do
  collection do
    post 'paypay_save'
  end
end

controllerに新しくメソッド追加

shops_controller.rb
def paypay_save
   @shops = set_paypay_shops
   shops = []
   @shops.each do |shop|
     shops << Shop.new(name: shop)
   end
   # DBアクセス一回で配列を保存できるgem activerecord-import
   Shop.import shops
 end

viewも保存用のactionを使えるように設定

new.html.slim
h2 お店を追加する
= render 'form'
= link_to "paypayのキャンペーンを表示する", new_shop_path("paypay")
- if @shops.present?
  // button_toを追加
  = button_to "paypayのキャンペーンショップを追加する", paypay_save_shops_path
  - @shops.each do |shop|
    p = shop

これでviewからキャンペーンショップを追加するボタンを押せば完成です!!

今後はDBのバリデーションとかもやりたいですね。笑

参考

https://morizyun.github.io/blog/ruby-nokogiri-scraping-tutorial/index.html
https://qiita.com/superman9387/items/1981a24664b260e77238
https://qiita.com/makicamel/items/b6d4f3d2661fc66103ed

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

has_and_belongs_to_manyメソッドを使用してモデルを関連付けする

has_and_belongs_to_manyメソッドを使って多対多のモデルの関連付けからレコードを作成するまでの手順を書いていきます。

記事執筆時のRailsとRubyのバージョンは以下のとおりです。

$ rails -v
Rails 6.0.3.1
$ ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin18]

has_and_belongs_to_many関連付け

has_and_belongs_to_many関連付けでは、多対多のモデルをつなぐ中間テーブルを作成します。has_many :through関連付けのように中間モデルは作成しません。

モデルの関連付けのイメージ図(Railsガイドから引用)
Active Record の関連付け - Railsガイド
image.png

assembliesとpartsが多対多で関連付けるモデルで、assemblies_partsがそれらの中間テーブルです。

モデルの関連付け

上記のモデルを以下のように具体化して説明します。
assembliesモデル→Userモデル
partsモデル→Bookモデル
assemblies_parts→books_usersテーブル

ユーザー(User)が読んだ本(Book)を登録するアプリを作成するとします。
1人のUserは複数のBookを登録でき、同じBookは複数のUserから登録されるため、多対多の関係になります。

UserモデルとBookモデルを関連づけるために、それぞれのモデルに以下の定義を追加します。

class User < ApplicationRecord
  (中略)
  has_and_belongs_to_many :books
end
class Book < ApplicationRecord
  (中略)
  has_and_belongs_to_many :users
end

UserとBookを紐づけるために中間テーブルを作成します。モデルは作成しないので、マイグレーションファイルのみ作成します。

rails g migration create_books_users book:references user:references

マイグレーションファイルが作成されるので、rails db:migrateを実行します。

class CreateBooksUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :books_users do |t|
      t.references :book, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
    end
  end
end

レコードの作成

本(Book)を登録するアクションをコントローラーに作成します。

以下の例ではbuildメソッドでBookオブジェクトの作成、saveメソッドでBookオブジェクトをDBに保存、<<メソッドで中間テーブルにレコードを作成しています。
buildメソッド、<<メソッドはhas_and_belongs_to_manyで宣言したことにより使えるようになるメソッドです。
Bookオブジェクトの作成とDBへの保存の処理を分ける必要がない場合は、buildメソッドと<<メソッドの代わりにcreateメソッドを使ってオブジェクトの作成から保存までまとめて実行することもできます。
has_and_belongs_to_manyで追加されるメソッド - Railsガイド

class BooksController < ApplicationController
  (中略)
  def create
    # 登録するBookのレコードに紐づけたい任意のUserオブジェクトをインスタンス変数に設定
    @user = User.first
    # Userと紐づくBookオブジェクトを作成(book_paramsはストロングパラメーター)
    @book = @user.books.build(book_params)

    # DBに保存
    if @book.save
      # 中間テーブルにレコードを作成
      @user.books << @book 
      redirect_to @book
    else
      render :new
    end
  end
  (中略)
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker + Rails で Bundler v2.X を使う

Gemfile.lock の BUNDLED WITH と Docker イメージ内の bundler バージョンに乖離があるとエラーになる。

https://qiita.com/tanakaworld/items/e15ff9dbdd4b628378c2

次の通り対策ができる。

  1. イメージ内の gem をアップデート gem update --system
  2. 任意のバージョンの bundler をインストール gem install bundler -v <バージョン>

例:

Dockerfile
FROM ruby:2.6.2

ENV APP_HOME /app
WORKDIR $APP_HOME

COPY Gemfile $APP_HOME/Gemfile
COPY Gemfile.lock $APP_HOME/Gemfile.lock

ENV BUNDLER_VERSION 2.1.0
RUN gem update --system \
    && gem install bundler -v $BUNDLER_VERSION \
    && bundle install -j 4

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

ほとんど意味のないコピペ記事はどこから来ているのか

active_hash に関する記事は少ない。
https://kossy-web-engineer.hatenablog.com/entry/2019/01/08/205702
で知って少し調べてみた。

Qiita では
https://qiita.com/haruya_hamasaki/items/3cd0b780fb1f076a9bb8
https://qiita.com/Toman1223/items/8633142312bfa886d50b
という記事が見つかったが、ほとんど追加の内容がない。

self.data のインデントは同じ…。まぁ、同じデータを機械整形したら、同じになる気もする。

このような記事を投稿するインセンティブは誰にどのようにあるのだろうか…。

質(しつ)が悪く、質(たち)が悪い記事は、大抵参考文献とかは書いていない。先人から学び、自分も何かを残すという、根本がなっていない。

[2] pry(main) と数字が入るのと pry(main) を併用しているのって、どこかで自分が書いた部分とコピペが混じっているからではないかと勘繰ってしまう。

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

master failed to start, check stderr log for detailsの解決法

なにこれ

筆者がcapistranoでデプロイをするときに、
「master failed to start, check stderr log for details」
のエラーに7時間悩まされた話

01:57 unicorn:start
      01 $HOME/.rbenv/bin/rbenv exec bundle exec unicorn -c /var/www/MyNo…
      01 master failed to start, check stderr log for details

結論

自分の場合はかなり特例だと思います。
credentials.yml.encに以下の内容を追加したら解決しました。

ターミナル.
EDITOR='vim --wait' rails credentials:edit
credentials.yml
aws:
  AWS_ACCESS_KEY_ID: AKIAXK2VAZPGFVRBDTPV
  AWS_SECRET_ACCESS_KEY: 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

「master failed to start, check stderr log for details」で検索すると、
unicornのバージョン変更や編集権限の記事がヒットするのですが、
自分の場合は全く違いました。

経緯

VPCの関係でEC2インスタンスを作り直したのが発端です。
既存のアプリを新しいEC2に引っ越ししました。
ほとんど完成した状態でAWSのアクセスキーなどの記述がありました。

手順は後述。
ローカルで【bundle exec cap production deploy】
タイトルのエラーでコケる←ここで7時間ハマる

エラーログをよく見て原因を考える

ターミナル.
01:57 unicorn:stop
      unicorn is not running...
01:57 unicorn:start
      01 $HOME/.rbenv/bin/rbenv exec bundle exec unicorn -c /var/www/MyNo…
      01 master failed to start, check stderr log for details

#<Thread:0x00007fbc0da2b5e0@/Users/ohishikaido/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.21.0/lib/sshkit/runners/parallel.rb:10 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
        1: from /Users/ohishikaido/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.21.0/lib/sshkit/runners/parallel.rb:11:in `block (2 levels) in execute'
/Users/ohishikaido/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.21.0/lib/sshkit/runners/parallel.rb:15:in `rescue in block (2 levels) in execute'

Exception while executing as kaito@3.113.216.56: bundle exit status: 1 (SSHKit::Runner::ExecuteError)
bundle stdout: Nothing written
bundle stderr: master failed to start, check stderr log for details
(Backtrace restricted to imported tasks)

cap aborted!
SSHKit::Runner::ExecuteError: Exception while executing as kaito@3.113.216.56: bundle exit status: 1
bundle stdout: Nothing written
bundle stderr: master failed to start, check stderr log for details

Caused by:
SSHKit::Command::Failed: bundle exit status: 1
bundle stdout: Nothing written
bundle stderr: master failed to start, check stderr log for details
Tasks: TOP => unicorn:start
(See full trace by running task with --trace)

The deploy has failed with an error: Exception while executing as kaito@3.113.216.56: bundle exit status: 1
bundle stdout: Nothing written
bundle stderr: master failed to start, check stderr log for details

「01:57 unicorn:start」で失敗してる。
EC2でエラーログを確認してみる。

EC2.
cd /var/www/アプリ名/current/log/
cat unicorn.stderr.log

(ここだけ画像です!)
エラー文.png
aws_accses_key_id と aws_secret_accses_key がArgumentError??
どういうこと?
そういえば、carrierwaveでAWSのキーを使ってた。

carrierwave.rb
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'],
      aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
      region: 'ap-northeast-1'
    }

上記を以下に変更

carrierwave.rb
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: Rails.application.credentials.aws[:AWS_ACCESS_KEY_ID],
      aws_secret_access_key: Rails.application.credentials.aws[:AWS_SECRET_ACCESS_KEY],
      region: 'ap-northeast-1'
    }

そして、冒頭の結論に書いてあるcredentials.ymlに記述を行う。
commit&pushしてデプロイすると解決しました。

感想

一度carrierwave.rbのENV['AWS_ACCESS_KEY_ID']の記述をコメントアウトして
デプロイしてみたんですけど、同じエラーでコケました。
アクセスキーに関する記述があると、どこかでアクセスキーを書いてないとエラーが出るっていう感じなんですね。
勉強になりました!

余談

EC2で【sudo vim /etc/environment】を叩いて、
そちらにAWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYを書いても成功しました。
管理しやすいと思って、credentials.ymlに書くことにしました。

EC2からユニコーンが再起動できない!問題を解決

結論

unicorn.rbのapp_pathの指定を変えるだけ。

config/unicorn.rb
app_path = File.expand_path('/var/www/アプリ名')

経緯

起動中のユニコーンのプロセスを確認します。

EC2.
ps aux | grep unicorn

以下のやつしかいない。ok

EC2.
ec2-user 32720  0.0  0.2 110536  2180 pts/1    S+   22:49   0:00 grep --color=auto unicorn

【bundle exec unicorn_rails -c config/unicorn.rb -E production -D】
を叩いても、タイトルのエラーでコケます!
【bundle exec cap production deploy】が通るのに、
EC2のcurrentで通らない。。。謎みが深い。

コマンドをRAILS_SERVE_STATIC_FILES=1 unicorn_rails -c config/unicorn.rb -E production -Dに変えてもダメでした。。
sudo service mysqld restartもやりました。

エラー文.
bundler: failed to load command: unicorn_rails (/var/www/MyNote/shared/bundle/ruby/2.5.0/bin/unicorn_rails)
ArgumentError: config_file=config/unicorn.rb would not be accessible in working_directory=/var/www/MyNote/releases/current
(中略)
master failed to start, check stderr log for details

うーん、原因がよく分からない。
/var/www/MyNote/releases/currentっていうパスだから、
/MyNote/releases/current/が悪いのは分かる。
そんなパスは存在しないので。
でも、unicorn.rbでパス指定を別にしたら、他エラーが出てきてユニコーンが起動できない。笑

config/unicorn.rb
app_path = File.expand_path('../../', __FILE__)
working_directory "#{app_path}/current"
EC2.
bundler: failed to load command: unicorn_rails (/var/www/MyNote/shared/bundle/ruby/2.5.0/bin/unicorn_rails)

ArgumentError: config_file=config/unicorn.rb would not be accessible in working_directory=/var/www/MyNote/releases/20200628222655/current

bundle exec unicorn_rails -c config/unicorn.rb -E production -Dを叩いてコケる
~/MyNote/releases/20200628222655/currentってどこだよ(哲学)

しょうがないので絶対パスにしてみる。

config/unicorn.rb
app_path = File.expand_path('/var/www/アプリ名')

bundle exec unicorn_rails -c config/unicorn.rb -E production -Dを叩いてコケる
エラーログをcat log/unicorn.stderr.logで確認します!

log/unicorn.stderr.log
bundler: failed to load command: unicorn_rails (/var/www/MyNote/shared/bundle/ruby/2.5.0/bin/unicorn_rails)
ArgumentError: Already running on PID:31334 (or pid=/var/www/MyNote/shared/tmp/pids/unicorn.pid is stale)

PIDが生きてるのでkillします。

ps aux | grep unicorn
kill -9 プロセス番号

ユニコーン起動できた〜

ec2-user   612 38.0 11.3 509012 114744 ?       Sl   23:43   0:01 unicorn_rails master -c config/unicorn.rb -E production -D                                                                                            
ec2-user   617  0.0 10.6 510088 107676 ?       Sl   23:43   0:00 unicorn_rails worker[0] -c config/unicorn.rb -E production -D                                                                                         
ec2-user   622  0.0  0.2 110536  2144 pts/1    S+   23:43   0:00 grep --color=auto unicorn

今までの手順はEC2からvimでいじっただけなので、bundle exec cap production deploy
を叩いてみる。

00:48 deploy:log_revision
      01 echo "Branch master (at f7a05267807ec5da99859b9a14bf49c494547dac) deployed as release 202…
    ✔ 01 ec2-user@13.114.24.148 0.469s
ohishikaido@ohishi-MacBook-Air my_app % 

通りました笑

経緯の手順

Gemfile.
# 既にあるグループ内に追記
group :development, :test do
  gem 'pry-rails'
  gem 'capistrano'
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano-rails'
  gem 'capistrano3-unicorn'
  gem 'capistrano-rails-console'
end

# 以下全てをGemfile下部にコピー
group :production do
  gem 'unicorn', '5.4.1'
end
config/deploy/production.rb
server '自身のElasticIP', user: 'ec2-user', roles: %w{app db web}
config/unicorn.rb(新規作成)
# ファイルが無いのでconfigフォルダにunicorn.rbを新規作成します

app_path = File.expand_path('../../../', __FILE__)

worker_processes 1

working_directory "#{app_path}/current"
pid "#{app_path}/shared/tmp/pids/unicorn.pid"
listen "#{app_path}/shared/tmp/sockets/unicorn.sock"
stderr_path "#{app_path}/shared/log/unicorn.stderr.log"
stdout_path "#{app_path}/shared/log/unicorn.stdout.log"

timeout 60

preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true

check_client_connection false

run_once = true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.connection.disconnect!

  if run_once
    run_once = false # prevent from firing again
  end

  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exist?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH => e
      logger.error e
    end
  end
end

after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end
config/deploy.rb
# Gemfile.lockを見てcapistranoのバージョンを入れる
lock 'xx.xx.x'

# 自身のアプリ名、リポジトリ名を記述
set :application, 'アプリ名'
set :repo_url,  'git@github.com:githubのユーザー名/リポジトリ名.git'

set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads')

set :rbenv_type, :user
set :rbenv_ruby, '2.5.1'

# chat-spaceで使ったpemを指定
set :ssh_options, auth_methods: ['publickey'],
                  keys: ['~/.ssh/xxx.pem']

set :unicorn_pid, -> { "#{shared_path}/tmp/pids/unicorn.pid" }
set :unicorn_config_path, -> { "#{current_path}/config/unicorn.rb" }
set :keep_releases, 5

set :linked_files, %w{ config/master.key }

after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:stop'
    invoke 'unicorn:start'
  end

  desc 'upload master.key'
  task :upload do
    on roles(:app) do |host|
      if test "[ ! -d #{shared_path}/config ]"
        execute "mkdir -p #{shared_path}/config"
      end
      upload!('config/master.key', "#{shared_path}/config/master.key")
    end
  end
  before :starting, 'deploy:upload'
  after :finishing, 'deploy:cleanup'
end
config/database.yml
production:
  <<: *default
  database: 本番環境のDB名
  username: root
  password: password
  socket: /var/lib/mysql/mysql.sock
  encoding: utf8

EC2で【sudo service mysqld start】を打つ
ローカルで【bundle exec cap production deploy】を打つ

unknown databaseが出るので、EC2の適当な場所で、
 【rails db:create RAILS_ENV=production】を打つ

EC2.
cd /var/www/アプリ名/releases
ll            #=> 日付を表す数字の名前のフォルダが表示される 例:20200218063515
cd 一番下の数字 #=> 例 cd 20200218063515
rails db:create RAILS_ENV=production

EC2で【sudo vim /etc/nginx/conf.d/rails.conf】を叩いて編集

rails.conf
upstream app_server {
  server unix:/var/www/アプリ名/shared/tmp/sockets/unicorn.sock;
}

server {
  listen 80;
  server_name ご自身のElastic IP;

  root /var/www/アプリ名/current/public;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
    root   /var/www/アプリ名/current/public;
  }

  try_files $uri/index.html $uri @unicorn;

  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
  }

  error_page 500 502 503 504 /500.html;
}

EC2で【sudo service nginx start】を打つ
ローカルで【bundle exec cap production deploy】を叩いてコケる。

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

railsでモデル作るときによく使うコマンド備忘録

rails generate model

https://railsdoc.com/rails#rails_generate_model

基本

# rails generate model NAME [field[:type][:index] field[:type][:index]] [options]
$ rails generate model AdminUser name:string user:references mail:string:unique age:integer:index 
  • model名は単数形
  • よく使う型:string, integer, references
  • :indexに使える値: index, unique
  • 複雑なインデックス作るくらいならマイグレーションファイル作って直にマイグレーションファイルいじるほうが速いかもしれない

マイグレーションファイル変更

https://railsdoc.com/page/create_table
https://railsguides.jp/active_record_migrations.html

基本

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
      t.integer :hoge,   limit: 1, null: false, default: 0

      t.timestamps
    end
  end
end

主キー

主キーを指定してテーブルを作成

create_table(:objects, primary_key: 'guid') do |t|
  t.column :name, :string, limit: 80
end

この場合id: falseは不要(参考)

複合主キーを使う場合

https://www.kaqiita.com/entry/2019/02/17/104923
id以外の主キーを作るときとはルールが違う

  def change
    create_table :hoges, id: false, primary_key: [:fuga, :piyo] do |t|
      t.integer fuga
      t.integer piyo
    end
  end

プライマリーキーの無いテーブルを作成

create_table(:categories_suppliers, id: false) do |t|
  t.column :category_id, :integer
  t.column :supplier_id, :integer
end

インデックス

https://railsdoc.com/migration#add_index

複数キーインデックス

add_index :accounts, [:name, :surname]

ユニークなインデックス

add_index :users, [:name, :employee_id], unique: true

ソート順でインデックスを作成

add_index :accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc}

マイグレーション実行

$ rake db:migrate

モデル修正

テーブル変更
カラム変更
changeメソッドのバリエーション

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

railsでモデル作るときによく使うコマンド備忘録(rails generate model関連)

rails generate model

https://railsdoc.com/rails#rails_generate_model

基本

# rails generate model NAME [field[:type][:index] field[:type][:index]] [options]
$ rails generate model AdminUser name:string user:references mail:string:unique age:integer:index 
  • model名は単数形
  • よく使う型
    • string
    • integer
    • references
  • :indexに使える値:
    • index,
    • uniq
      • uniqueではないので注意(rails6だからなのか? 他のバージョンで未検証)
      • ユニークインデックスを作成
  • 複雑なインデックス作るくらいならマイグレーションファイル作って直にマイグレーションファイルいじるほうが速いかもしれない

マイグレーションファイル変更

https://railsdoc.com/page/create_table
https://railsguides.jp/active_record_migrations.html

基本

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
      t.integer :hoge,   limit: 1, null: false, default: 0

      t.timestamps
    end
  end
end

主キー

主キーを指定してテーブルを作成

create_table(:objects, primary_key: 'guid') do |t|
  t.column :name, :string, limit: 80
end

この場合id: falseは不要(参考)

複合主キーを使う場合

https://www.kaqiita.com/entry/2019/02/17/104923
id以外の主キーを作るときとはルールが違う

  def change
    create_table :hoges, id: false, primary_key: [:fuga, :piyo] do |t|
      t.integer fuga
      t.integer piyo
    end
  end

プライマリーキーの無いテーブルを作成

create_table(:categories_suppliers, id: false) do |t|
  t.column :category_id, :integer
  t.column :supplier_id, :integer
end

インデックス

https://railsdoc.com/migration#add_index

複数キーインデックス

add_index :accounts, [:name, :surname]

ユニークなインデックス

add_index :users, [:name, :employee_id], unique: true

ソート順でインデックスを作成

add_index :accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc}

マイグレーション実行

$ rake db:migrate

モデル修正

テーブル変更
カラム変更
changeメソッドのバリエーション

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

【devise】rails メモ 基本設定 初期設定

【ゴール】

devise 初期、基本設定まとめ
意外と、ごちゃっとなってしまうので、、、

【メリット】

■ 作業効率UP
■ devise理解度向上

【開発環境】

■ Mac OS catalina
■ Ruby on Rails (5.2.4.2)
■ Virtual Box:6.1
■ Vagrant: 2.2.7

【コマンド】

deviseを初期化

$ rails g devise:install

※エラーが出れば、
$ bundle exec spring stop

modelを作成

$ rails g devise:アプリ名

各viewを作成

$ rails g devise views アプリ名

各controller作成

$ rails g controllers devise:アプリ名

メール承認機能作成の場合

※Confirmableのコメントアウト外す

db/migrate/devise_creat_アプリ名.rb
## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

APIのログイン機能作成の場合

※Trackableのコメントアウト外す

db/migrate/devise_creat_アプリ名.rb
## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

以上

【合わせて読みたい】

■ 【環境変数】 rails 環境変数とは
https://qiita.com/tanaka-yu3/items/7bf03fee906b80367be9

■ 最速 rails mail devise ウェルカムメール送信機能実装 action mailer不要 一番簡単
https://qiita.com/tanaka-yu3/items/2def7760fd67fe73091a

■ 【devise】 管理者 ユーザー ログイン、ログアウト分ける
https://qiita.com/tanaka-yu3/items/046bf30e08e48fbc42ca

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

【Vue Rails】Vue + Railsで"Hello Vue!"表示 

Vue + Railsアプリ作成

◆ Railsアプリ作成

// "-webpack=vue"オプションでVue.js使用可能
$ rails new <アプリケーション名> -webpack=vue

◆ model作成

// カラム名:name データ型:text
$ rails g model sample name:text

◆ migrationファイル編集(Hello.Vue!表示には不要)

db/migrate/20200627045139_create_sample.rb
class CreateSample < ActiveRecord::Migration[6.0]
  def change
    create_table :sample do |t|
      t.text :name, null: false, default: ""
    end
  end
end

◆ マイグレーション

$ rails db:create      //データベース作成
$ rails db:migrate    //マイグレーション実施

◆ controller作成

app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
  end
end

◆ routes.rb編集

config/routes.rb
Rails.application.routes.draw do
  root to: 'home#index'
end

◆ index.html.erb編集

app/views/home/index.html.erb
<%= javascript_pack_tag 'hello_vue' %>
<%= stylesheet_pack_tag 'hello_vue' %>

◆ hello.vue.js(デフォルトで設定済)

app/javascript/packs/hello_vue.js
import Vue from 'vue'
import App from '../app.vue'

document.addEventListener('DOMContentLoaded', () => {
  const el = document.body.appendChild(document.createElement('hello'))
  const app = new Vue({
    el,
    render: h => h(App)
  })

  console.log(app)
})

◆ app.vue(デフォルトで設定済)

app/javascript/app.vue
<template>
  <div id="app">
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      message: "Hello Vue!"
    }
  }
}
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: center;
}
</style>

備忘録

◆ before_action

  • メソッドを定義して、before_actionにセットする
login_controller.rb
class LoginController < ApplicationController
  before_action :set_answer

  def set_answer
    @sample = "Hello World!"  
  end
end

◆ rescue_from

  • 例外処理。エラー処理を行う画面を設定する
  • app/controller/application_controller.rbに記述する
app/controller/application_controller.rb
class ApplicationController < ActionController::Base

  rescue_from ActiveRecord::RecordNotFound, with: :rescue404

end

遭遇したエラー

◆ エラー内容①

Webpacker::Manifest::MissingEntryError in Home#index

解決策:Webpackインストール

$ yarn
$ bin/yarn
$ webpack
$ webpack

◆ エラー内容②

Error: vue-loader requires @vue/compiler-sfc to be present in the dependency tree.

解決策:vue-loaderダウングレード

$ npm remove vue-loader
$ npm install --save vue-loader@15.9.2
$ yarn add vue-loader@15.9.2

◆ エラー内容③

Sprockets::Rails::Helper::AssetNotFound in Home#index
 <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

解決策:app/views/layouts/application.html.erb編集

app/views/layouts/application.html.erb
<!-- javascript_include_tagの行を削除 -->
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

参考文献

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

【Rails】フリマアプリ商品編集機能について(プレビュー編集・DB更新)

はじめに

草野と申します。
今回の投稿は、プログラミングスクールでチーム開発にて行ったフリーマーケット系ECサイトのクローンアプリ商品編集機能についてです。自分用のメモのため、文章は拙いですが、少しでも初学者の助けになればと考えています。
内容は、表題にもあるとおり、プレビュー編集とDB更新についてです。未熟な点も多いと思います。不備等ありましたらご指摘ください。随時改善して行こうと思います。ちなみに私は、スクール自体は卒業しており、学習した内容の振り返りとして投稿させて頂いております。

完成品

商品編集画面(プレビュー画像部)

Image from Gyazo

商品編集画面(カテゴリー部)

Image from Gyazo

商品編集画面(販売手数料・利益部)

Image from Gyazo

更新成功時の遷移画面

Image from Gyazo

実装手順

1.ルーティング編集

  • update_doneのルート設定(更新成功時の遷移画面)

2.コントローラー編集

  • editメソッド設定
  • updateメソッド設定
    • エラーハンドリング
    • 画像削除
  • update_daneメソッド設定

3.ビュー編集・作成

  • プレビュー画像呼び出し
  • カテゴリー呼び出しの調整
  • 更新成功時の遷移画面

4.JS編集

  • プレビュー画像及びinputタグの生成、削除
  • 販売手数料・利益の表示

1.ルーティング編集

update_doneルートを生成します。
これは、更新成功時の遷移画面を表示するためのルーティングです。

config/routes.rb 
  resources :items do
    resources :comments,  only: [:create, :destroy]
    resources :favorites, only: [:create, :destroy]
    collection do
      get 'get_category_children', defaults: { fomat: 'json'}
      get 'get_category_grandchildren', defaults: { fomat: 'json'}
      get 'search'
      get 'post_done'
      get 'delete_done'
      get 'detail_search'
      get 'update_done' # これを追加
    end
  end

2.コントローラー編集

今回編集したコントローラーの記述は下記の通りです。

app/controller/items_controller.rb 
class ItemsController < ApplicationController
before_action :category_parent_array, only: [:new, :create, :edit]
before_action :set_item, only: [:show, :edit, :update, :destroy]
before_action :show_all_instance, only: [:show, :edit, :destroy]

# 中略

  def edit
    grandchild = @item.category
    child = grandchild.parent
    if @category_id == 46 or @category_id == 74 or @category_id == 134 or @category_id == 142 or @category_id == 147 or @category_id == 150 or @category_id == 158
    else
     @parent_array = []
     @parent_array << @item.category.parent.parent.name
     @parent_array << @item.category.parent.parent.id
    end
     @category_children_array = Category.where(ancestry: child.ancestry)
     @child_array = []
     @child_array << child.name
     @child_array << child.id

     @category_grandchildren_array = Category.where(ancestry: grandchild.ancestry)
     @grandchild_array = []
     @grandchild_array << grandchild.name
     @grandchild_array << grandchild.id

  end

  def update
    if item_params[:images_attributes].nil?
      flash.now[:alert] = '更新できませんでした 【画像を1枚以上入れてください】'
      render :edit
    else
      exit_ids = []
      item_params[:images_attributes].each do |a,b|
        exit_ids << item_params[:images_attributes].dig(:"#{a}",:id).to_i
      end
      ids = Image.where(item_id: params[:id]).map{|image| image.id }
      delete__db = ids - exit_ids
      Image.where(id:delete__db).destroy_all
      @item.touch
      if @item.update(item_params)
        redirect_to  update_done_items_path
      else
        flash.now[:alert] = '更新できませんでした'
        render :edit
      end
    end
  end

  def update_done
    @item_update = Item.order("updated_at DESC").first
  end

# 中略

  private
  def item_params
    params.require(:item).permit(:name, :item_explanation, :category_id, :item_status, :auction_status, :delivery_fee, :shipping_origin, :exhibition_price,:brand_name, :days_until_shipping, images_attributes: [:image, :_destroy, :id]).merge(user_id: current_user.id)
  end

  def set_item
    @item = Item.find(params[:id])
  end

  def category_parent_array
    @category_parent_array = Category.where(ancestry: nil).each do |parent|
    end
  end

  def show_all_instance
    @user = User.find(@item.user_id)
    @images = Image.where(item_id: params[:id])
    @images_first = Image.where(item_id: params[:id]).first
    @category_id = @item.category_id
    @category_parent = Category.find(@category_id).parent.parent
    @category_child = Category.find(@category_id).parent
    @category_grandchild = Category.find(@category_id)
  end
end

まずeditメソッドを設定します。
他のメソッドで使用しているインスタンス変数を利用するのでリファクタリングのため、before_actionから呼び出しがあります。
使用しているインスタンス変数は下記の通りです。
② 親カテゴリーのnameとidが代入された配列
③ categoryモデル内の全ての子カテゴリー
④ 子カテゴリーのnameとidが代入された配列
⑤ categoryモデル内の全ての孫カテゴリー
⑥ 孫カテゴリーのnameとidが代入された配列
⑦ 該当商品情報
⑧ categoryモデル内の全ての親カテゴリー
⑨ 該当商品の画像
⑩ 該当商品のcategory_id(孫の数値)

app/controller/items_controller.rb 
class ItemsController < ApplicationController
before_action :category_parent_array, only: [:new, :create, :edit]
before_action :set_item, only: [:show, :edit, :update, :destroy]
before_action :show_all_instance, only: [:show, :edit, :destroy]

#中略

  def edit
    # ▼ ①ここで該当商品の子・孫カテゴリーを変数へ代入
    grandchild = @item.category
    child = grandchild.parent
    if @category_id == 46 or @category_id == 74 or @category_id == 134 or @category_id == 142 or @category_id == 147 or @category_id == 150 or @category_id == 158
    else
     # ② ▼ 親カテゴリーのnameとidを配列代入
     @parent_array = []
     @parent_array << @item.category.parent.parent.name
     @parent_array << @item.category.parent.parent.id
    end
     # ③ ▼ 子カテゴリーを全てインスタンス変数へ代入
     @category_children_array = Category.where(ancestry: child.ancestry)
     # ④ ▼ 子カテゴリーのnameとidを配列代入
     @child_array = []
     @child_array << child.name # ⑤で生成した変数を元にname・idを取得
     @child_array << child.id
     # ⑤ ▼ 孫カテゴリーを全てインスタンス変数へ代入
     @category_grandchildren_array = Category.where(ancestry: grandchild.ancestry) 
     # ⑥ ▼ 孫カテゴリーのnameとidを配列代入
     @grandchild_array = []
     @grandchild_array << grandchild.name # ⑤で生成した変数を元にname・idを取得
     @grandchild_array << grandchild.id
  end
  end

#中略

  private
  def item_params
    params.require(:item).permit(:name, :item_explanation, :category_id, :item_status, :auction_status, :delivery_fee, :shipping_origin, :exhibition_price,:brand_name, :days_until_shipping, images_attributes: [:image, :_destroy, :id]).merge(user_id: current_user.id)
  end

  def set_item
    @item = Item.find(params[:id])                                # ⑦ 該当の商品情報をインスタンス変数へ代入
  end

  def category_parent_array
    @category_parent_array = Category.where(ancestry: nil)        # ⑧ 親カテゴリーを全てインスタンス変数へ代入
  end

  def show_all_instance
    @user = User.find(@item.user_id)
    @images = Image.where(item_id: params[:id])                   # ⑨ 該当商品の画像をインスタンス変数へ代入
    @images_first = Image.where(item_id: params[:id]).first
    @category_id = @item.category_id                              # ⑩ 該当商品のレコードからカテゴリーidを取得し、インスタンス変数へ代入(この際に取得するidは孫カテゴリーidです。)
    @category_parent = Category.find(@category_id).parent.parent                    
    @category_child = Category.find(@category_id).parent
    @category_grandchild = Category.find(@category_id)
  end

それぞれを分類分けして並べ替えると下記の通りになります。

商品情報をinputタグに初期値として表示させるためのもの
 ⑦ 該当商品情報
商品画像をプレビューに初期値として表示させるためのもの
 ⑨ 該当商品の画像
カテゴリーをinputタグに初期値として表示させるためのもの

  • 親・子・孫のname・idを取得し、ビュー側のcollection_selectで利用する情報
      ⑩ 該当商品のcategory_id(孫の数値)
      ② 親カテゴリーのnameとidが代入された配列
      ④ 子カテゴリーのnameとidが代入された配列
      ⑥ 孫カテゴリーのnameとidが代入された配列

  • 再入力時にビュー側のcollection_selectで利用する情報
      ⑧ categoryモデル内の全ての親カテゴリー
      ③ categoryモデル内の全ての子カテゴリー
      ⑤ categoryモデル内の全ての孫カテゴリー

次にupdateメソッドの設定です。
editと同様に更新したい商品情報については、before_actionにて呼び出しを行っています。

app/controller/items_controller.rb 
class ItemsController < ApplicationController
before_action :set_item, only: [:show, :edit, :update, :destroy]

# 中略

  def update
    # ①
    if item_params[:images_attributes].nil?
      flash.now[:alert] = '更新できませんでした 【画像を1枚以上入れてください】'
      render :edit
    else
    # ②
      exit_ids = []
      item_params[:images_attributes].each do |a,b|
        exit_ids << item_params[:images_attributes].dig(:"#{a}",:id).to_i
      end
      ids = Image.where(item_id: params[:id]).map{|image| image.id }
    # ③
      delete__db = ids - exit_ids
      Image.where(id:delete__db).destroy_all
    # ④
      @item.touch
      if @item.update(item_params)
        redirect_to  update_done_items_path
      else
        flash.now[:alert] = '更新できませんでした'
        render :edit
      end
    end
  end


# 中略

  private
  def item_params
    params.require(:item).permit(:name, :item_explanation, :category_id, :item_status, :auction_status, :delivery_fee, :shipping_origin, :exhibition_price,:brand_name, :days_until_shipping, images_attributes: [:image, :_destroy, :id]).merge(user_id: current_user.id)
  end

  def set_item
    @item = Item.find(params[:id])
  end
end

まずこちらの記述は、画像が1枚もない時に更新できないようにif文でエラーハンドリングを記述しています。
item_params[:images_attributes].nil?の記述でparams内の画像が空か確かめています。
.nil?メソッドで空の場合はtureとなり、renderで編集画面に戻りflash.now[:alert]でエラーメッセージを表示します。

    if item_params[:images_attributes].nil?
      flash.now[:alert] = '更新できませんでした 【画像を1枚以上入れてください】'
      render :edit
    else

先ほどのif文でfalseとなった場合に②が動きます。
記述している内容は、exit_idsが更新ボタンを押した時点で入力されている画像のid、idsがDB内に保存されている更新前画像のidとなります。
exit_idsという配列を生成し、item_params[:images_attributes]という多次元配列内に含まれるidの値を取り出したいのでeach文でキーと値を順番に展開します。(|a,b| → a キーのこと b 値のこと)
そして多次元配列から値を取り出すために使用するのがdigメソッドです。
item_params[:images_attributes](多次元配列).dig(:"#{a(親キー)}",:id(子キー)).to_i(数値にする)という記述でidを取り出し、配列に代入します。
そしてidsにはDBから更新前の該当するレコードを取得し、mapメソッドにてidを抽出し代入します。

      exit_ids = []
      item_params[:images_attributes].each do |a,b|
        exit_ids << item_params[:images_attributes].dig(:"#{a}",:id).to_i
      end
      ids = Image.where(item_id: params[:id]).map{|image| image.id }

ちなみにbinding.pryを使用してitem_params[:images_attributes]の中身を確認すると下記のように表示されます。画像1枚で更新ボタンをクリックしました。②のexit_idsに代入したい値は、子の配列内にある"322"という値です。

ターミナル(コンソール起動)
[1] pry(#<ItemsController>)> item_params[:images_attributes]
=> <ActionController::Parameters {"0"=><ActionController::Parameters {"id"=>"322"} permitted: true>} permitted: true>

③では、先ほどのexit_idsとidsを比較し、DBから初期値として編集画面に呼び出されていた画像を削除した場合にDB内の該当データを削除します。idsからexit_idsを引くことで削除されているidだけ残すことができます。それをdelete__dbに代入し、それを元にDBからレコードを検索し、destroy_allメソッドを使って削除します。
_allとしているのは複数レコードの場合も削除できるようにするためです。

      delete__db = ids - exit_ids
      Image.where(id:delete__db).destroy_all

④では、商品情報の更新を行っています。if文のエラーハンドリングにより更新できた場合には、update_dineルートを通り更新成功を伝える画面に遷移します。更新でなかった場合には編集画面に戻り、エラーメッセージを表示します。
一番最初の行に記述している@item.touchはitemsテーブルのupdate_atカラム(更新日時)も含めて更新するためのものです。
これを記述する理由は、後ほどご説明します。

      @item.touch
      if @item.update(item_params)
        redirect_to  update_done_items_path
      else
        flash.now[:alert] = '更新できませんでした'
        render :edit
      end

次にupdate_doneメソッドの設定です。
更新成功を伝える画面には更新した商品詳細ページのリンクを設置しています。
先ほどのupdateメソッドの④で@item.touchを記述することによりitemsテーブルのupdate_atカラム(更新日時)を更新しました。orderメソッド、firstメソッドを使い、update_atカラム内を降順に一番目のものを@item_updateに代入します。

app/controller/items_controller.rb 
  def update_done
    @item_update = Item.order("updated_at DESC").first
  end

3.ビュー編集

全ての記述を載せると長くなってしまうのでここでは割愛して記述させて頂きます。

app/views/items/_form_edit.html.haml 
# ▼ 商品画像についての記載
.new__page__header
  = link_to image_tag("logo/logo.png", alt: "logo"), root_path
= form_for @item do |f|
  = render 'layouts/error_messages', model: f.object
  .name__field#1
    .form__label
      .lavel__name
        出品画像
      .lavel__Required
        [必須]
  #image-box-1{class:"#{@images.last.id}"}
# ▼ ① プレビュー画像の表示
    - @images.each do |img|
      .item-image{id:img.id}
        = image_tag(img.image.url,{width:"188",height:"180"})
        .item-image__operetion
          .item-image__operetion--edit__delete__hidden 削除
    %label.img-label{for: "img-file"}
      #image-box__container{class:"item-num-#{@images.length}"}
        #append-js-edit
          = f.fields_for :images do |image|
            .js-file_group{"data-index" => "#{image.index}"}
              = image.file_field :image, type: 'file', value:"#{image.object.id}",style: "",  id:"img-file", class:'js-file-edit',name: "item[images_attributes][#{@item.images.count}][image]", data:{index:""}
        %i.fas.fa-camera

# 中略

# ▼ カテゴリーについての記載
  .append__category
    .category
      =f.collection_select :category_id, @category_children_array, :id, :name, {selected:@child_array}, {class:"serect_field"}
    - if @category_id == 46 or @category_id == 74 or @category_id == 134 or @category_id == 142 or @category_id == 147 or @category_id == 150 or @category_id == 158
      .category__grandchild#children_wrapper
        =f.collection_select :category_id, @category_grandchildren_array, :id, :name, {},{selected:@grandchild_array, id:"child__category",class:"serect_field"}
    - else
      .category__child#children_wrapper
        =f.collection_select :category_id, @category_children_array, :id, :name, {},{selected:@child_array, id:"child__category", class:"serect_field"}
      .category__grandchild#grandchildren_wrapper
        =f.collection_select :category_id, @category_grandchildren_array, :id, :name, {selected:@grandchild_array}, {class:"serect_field"}

# 省略

下記の記述でプレビュー画像を表示しています。
.item-image_operetion--editdelete_hidden 削除 のhiddenという記述がポイントです。js編集の際にご説明します。

    - @images.each do |img|
      .item-image{id:img.id}
        = image_tag(img.image.url,{width:"188",height:"180"})
        .item-image__operetion
          .item-image__operetion--edit__delete__hidden 削除

下記の記述でカテゴリーを表示しています。
if文で孫なしの場合と孫ありの場合で条件分岐させています。
collection_selectタグに中身の内{}を記述していますがこれはidを付与するにあたり、オプションを記述する際の引数の順番の関係で記述しています。

カテゴリー
  .append__category
    .category
      =f.collection_select :category_id, @category_children_array, :id, :name, {selected:@child_array}, {class:"serect_field"}
    - if @category_id == 46 or @category_id == 74 or @category_id == 134 or @category_id == 142 or @category_id == 147 or @category_id == 150 or @category_id == 158
      .category__grandchild#children_wrapper
        =f.collection_select :category_id, @category_grandchildren_array, :id, :name, {},{selected:@grandchild_array, id:"child__category",class:"serect_field"}
    - else
      .category__child#children_wrapper
        =f.collection_select :category_id, @category_children_array, :id, :name, {},{selected:@child_array, id:"child__category", class:"serect_field"}
      .category__grandchild#grandchildren_wrapper
        =f. :category_id, @category_grandchildren_array, :id, :name, {selected:@grandchild_array}, {class:"serect_field"}

下記の記述は、更新成功時遷移画面のビューファイルになります。

app/views/items/_form_edit.html.haml 
= render "top/header"
.done#fullsize
  .done__title
    商品情報を更新しました
  .done__backlink
    = link_to 'トップページへ戻る', root_path, class: 'link'
  .done__backlink
    = link_to '更新した商品を確認する', item_path(@item_update), class: 'link'
  .done__backlink
    = link_to '出品中の商品一覧を見る', users_path, class: 'link'
= render "top/lower-photo"
= render "top/footer"
= render "top/btn"

4.JS編集

下記の通りjsファイルを編集します。

app/assets/javascript/edit_items.js 
$(function(){
  var dataBox = new DataTransfer();
  var file_field = document.getElementById('img-file')
  $('#append-js-edit').on('change','#img-file',function(){
    $.each(this.files, function(i, file){
      //FileReaderのreadAsDataURLで指定したFileオブジェクトを読み込む
      var fileReader = new FileReader();
      //DataTransferオブジェクトに対して、fileを追加
      dataBox.items.add(file)
      var num = $('.item-image').length + 1 + i
      var aaa = $('.item-image').length + i
// ①
      var image_id = Number($('#image-box-1').attr('class'))
      var append_div_count = Number($('div[id=1]').length) 
      var noreset_id = image_id + append_div_count

      fileReader.readAsDataURL(file);
     //画像が10枚になったら超えたらボックスを削除する
      if (num == 10){
        $('#image-box__container').css('display', 'none')
      }
      //読み込みが完了すると、srcにfileのURLを格納
      fileReader.onloadend = function() {
        var src = fileReader.result
// ②
        var html= `<div class='item-image' data-image="${file.name}" data-index="${aaa}" id="${noreset_id-1}">
                    <div class=' item-image__content'>
                      <div class='item-image__content--icon'>
                        <img src=${src} width="188" height="180" >
                      </div>
                    </div>
                    <div class='item-image__operetion'>
                      <div class='item-image__operetion--edit__delete__file'>削除</div>
                    </div>
                  </div>`
        const buildFileField1 = (num)=> {
// ③
          const html = `<div  class="js-file_group" data-index="${num}" id=1>
                          <input class="js-file-edit" type="file"
                          name="item[images_attributes][${append_div_count+9}][image]"
                          id="img-file" data-index="${num}value="${noreset_id}" >
                        </div>`;
          return html;
        }
        $('.js-file-edit').removeAttr('id');
        //image_box__container要素の前にhtmlを差し込む
        $('.img-label').before(html);
        $('#append-js-edit').append(buildFileField1(num));
      };
      //image-box__containerのクラスを変更し、CSSでドロップボックスの大きさを変えてやる。
      $('#image-box__container').attr('class', `item-num-${num}`)
    });
  });
// ④
  // 10枚登録されていた場合にボックスを消す
  $(document).ready(function(){
    var image_num = $('.item-image').length
    if (image_num==10){
      $('#image-box__container').css('display', 'none')
    }
  });
// ⑤
  $(document).ready(function(){
    $('.js-file-edit').removeAttr('id');
    var num = $('.item-image').length - 1
    var image_id = Number($('#image-box-1').attr('class'))
    var append_div_count = Number($('div[id=1]').length) 
    var noreset_id = image_id + append_div_count
    const buildFileField = (num)=> {
      const html = `<div  class="js-file_group" data-index="${num}" id=1>
                      <input class="js-file-edit" type="file"
                      name="item[images_attributes][100][image]"
                      id="img-file" data-index="${num}" value="${noreset_id}" >
                    </div>`;
      return html;
    }
    $('#append-js-edit').append(buildFileField(num));
  });
// ⑥
  $(document).on("click", '.item-image__operetion--edit__delete__hidden', function(){
    //削除を押されたプレビュー要素を取得
    var target_image = $(this).parent().parent();
    //削除を押されたプレビューimageのfile名を取得
    var target_id = $(target_image).attr('id');
    var target_image_file = $('input[value="'+target_id+'"][type=hidden]');
    //プレビューを削除
    target_image.remove()
    target_image_file.remove()
    //image-box__containerクラスをもつdivタグのクラスを削除のたびに変更
    var num = $('.item-image').length
    $('#image-box__container').show()
    $('#image-box__container').attr('class', `item-num-${num}`)
  })
// ⑦
  $(document).on("click", '.item-image__operetion--edit__delete__file', function(){
    //削除を押されたプレビュー要素を取得
    var target_image = $(this).parent().parent();
    var target_id = Number($(target_image).attr('id'));
    //削除を押されたプレビューimageのfile名を取得
    var target_image_file = $('#append-js-edit').children('div').children('input[value="'+target_id+'"][type=file]');
    //プレビューを削除
    target_image.remove()
    target_image_file.remove()
    //image-box__containerクラスをもつdivタグのクラスを削除のたびに変更
    var num = $('.item-image').length
    $('#image-box__container').show()
    $('#image-box__container').attr('class', `item-num-${num}`)
  })

1行目の記述で一番最後に保存された画像idを取得しimage_id変数に代入します。2行目でビューファイル内のdivでid=1と付与されているタグの数を数えappend_div_count変数に代入します。3行目でそれを足し合わせ、
noreset_id変数に代入します。noreset_idは、画像を追加した際に新たに表示されるinputタグのvalueオプションにセットするためのものです。これを利用して削除の動作を行います。また、プレビュー画像の親となるdivタグにも同じ数値のものを狙って削除するため、idオプションにセットします。(②)

      var image_id = Number($('#image-box-1').attr('class'))
      var append_div_count = Number($('div[id=1]').length) 
      var noreset_id = image_id + append_div_count

下記の記述は、画像データをinputタグに入力した際にイベント発火して生成されるプレビュー画像のHTMLです。

削除の記述でfileという記述がポイントです。ビュー編集時に言っていたhiddenの記述と見分け、新た生成されたinputタグなのか、最初から表示されているinputタグなのか判断します。
 var html= `<div class='item-image' data-image="${file.name}" data-index="${aaa}" id="${noreset_id-1}">
                    <div class=' item-image__content'>
                      <div class='item-image__content--icon'>
                        <img src=${src} width="188" height="180" >
                      </div>
                    </div>
                    <div class='item-image__operetion'>
                      <div class='item-image__operetion--edit__delete__file'>削除</div>
                    </div>
                  </div>`

下記の記述は、画像データをinputタグに入力した際にイベント発火して生成されるinputタグのHTMLです。

 const html = `<div  class="js-file_group" data-index="${num}" id=1>
                          <input class="js-file-edit" type="file"
                          name="item[images_attributes][${append_div_count+9}][image]"
                          id="img-file" data-index="${num}value="${noreset_id}" >
                        </div>`;

下記の記述は、readyメソッドにより画面がロード完了するとイベント発火し、プレビュー画像の数を数えて10枚だった場合に画像を入力するボックスを削除するという記述になります。

  // 10枚登録されていた場合にボックスを消す
  $(document).ready(function(){
    var image_num = $('.item-image').length
    if (image_num==10){
      $('#image-box__container').css('display', 'none')
    }
  });

下記の記述は、readyメソッドにより画面がロード完了するとイベント発火し、inputタグが生成される記述になります。これを行わない場合、最初のinputタグへの入力が、既存の表示されているinputタグに入力されてしまいズレが生じてしまうため、画面ロード時に生成する必要があります。

  $(document).ready(function(){
    $('.js-file-edit').removeAttr('id');
    var num = $('.item-image').length - 1
    var image_id = Number($('#image-box-1').attr('class'))
    var append_div_count = Number($('div[id=1]').length) 
    var noreset_id = image_id + append_div_count
    const buildFileField = (num)=> {
      const html = `<div  class="js-file_group" data-index="${num}" id=1>
                      <input class="js-file-edit" type="file"
                      name="item[images_attributes][100][image]"
                      id="img-file" data-index="${num}" value="${noreset_id}" >
                    </div>`;
      return html;
    }
    $('#append-js-edit').append(buildFileField(num));
  });

下記の記述は、editで呼び出した画像データが入力されているinputタグとプレビュー画像をプレビュー画像の左下に表示されている削除をクリックした際に削除するものです。

  $(document).on("click", '.item-image__operetion--edit__delete__hidden', function(){
    //削除を押されたプレビュー要素を取得
    var target_image = $(this).parent().parent();
    //削除を押されたプレビューimageのfile名を取得
    var target_id = $(target_image).attr('id');
    var target_image_file = $('input[value="'+target_id+'"][type=hidden]');
    //プレビューを削除
    target_image.remove()
    target_image_file.remove()
    //image-box__containerクラスをもつdivタグのクラスを削除のたびに変更
    var num = $('.item-image').length
    $('#image-box__container').show()
    $('#image-box__container').attr('class', `item-num-${num}`)
  })

下記の記述は、新たに画像がinputタグに入力された際に生成呼び出した画像データが入力されているinputタグと新たに生成されたプレビュー画像をプレビュー画像の左下に表示されている削除をクリックした際に削除するものです。

  $(document).on("click", '.item-image__operetion--edit__delete__file', function(){
    //削除を押されたプレビュー要素を取得
    var target_image = $(this).parent().parent();
    var target_id = Number($(target_image).attr('id'));
    //削除を押されたプレビューimageのfile名を取得
    var target_image_file = $('#append-js-edit').children('div').children('input[value="'+target_id+'"][type=file]');
    //プレビューを削除
    target_image.remove()
    target_image_file.remove()
    //image-box__containerクラスをもつdivタグのクラスを削除のたびに変更
    var num = $('.item-image').length
    $('#image-box__container').show()
    $('#image-box__container').attr('class', `item-num-${num}`)
  })

下記の記述は、価格が入力された際に販売手数料、販売利益を計算して出力するものです。
編集した箇所は、2段目、4段目に記述してる内容で、readyメソッドにより画面ロード時に販売手数料、販売利益を計算し、表示させるというものです。

app/assets/javascript/sales_commission.js 
$(function() {
  var input=$("#item_exhibition_price"),fee=1/10,feeIncluded=$("#sales_commission_price");
  input.on("input",function(){
    feeIncluded.text(Math.floor($(this).val() * fee).toLocaleString());
    if($('sales_commission_price').present!=0){
      sales_commission_price.append("");
    }
  });
  $(document).ready(function(){
    feeIncluded.text(Math.floor($("#item_exhibition_price").val() * fee).toLocaleString());
    if($('sales_commission_price').present!=0){
      sales_commission_price.append("");
    }
  });
});

$(function() {
  var input=$("#item_exhibition_price"),tax=9/10,salesProfit=$("#sales_profit_proce");
  input.on("input",function(){
    salesProfit.text(Math.ceil($(this).val() * tax).toLocaleString());
    if($('sales_commission_price').present!=0){
      sales_profit_proce.append("");
    }
  });
  $(document).ready(function(){
    salesProfit.text(Math.ceil($("#item_exhibition_price").val() * tax).toLocaleString());
    if($('sales_commission_price').present!=0){
      sales_profit_proce.append("");
    }
  });
});

これで編集機能完成です。
ここまで読んでくださり、ありがとうございます。

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

chat app.にてユーザー検索後、グループ追加を実装

はじめに・目的

インクリメンタルサーチ実装

1.API側準備

  • routing
routes.rb
resources :users, only: [:index]
  • users_controller.rbにindex定義
users_controllers.rb
class UsersController < ApplicationController
  def index
    respond_to do |format|
      format.html
      format.json
    end
  end
  • app/views/usersディレクトリにindex.json.jbuilderファイルを作成
index.json.jbuilder
json.array! @users do |user|
  json.id user.id
  json.name user.name
end

2.テキストフィールド作成

_form.html.haml
.chat-group-form__field
    .chat-group-form__field--left
      %label.chat-group-form__label{:for => "chat_group_チャットメンバーを追加"} チャットメンバーを追加
    .chat-group-form__field--right
      .chat-group-form__search.clearfix
        %input#user-search-field.chat-group-form__input{:placeholder => "追加したいユーザー名を入力してください", :type => "text"}/
      #user-search-result

  .chat-group-form__field.clearfix
    .chat-group-form__field--left
      %label.chat-group-form__label{:for => "chat_group_チャットメンバー"} チャットメンバー
    .chat-group-form__field--right
      #chat-group-users.js-add-user

3.テキストフィールドに入力するたび、イベント発火

users.js
$(function() {
  $("#user-search-field").on("keyup", function() {
    let input = $("#user-search-field").val();
    console.log(input);
  });
});

4.非同期通信ajax

users.js
  $("#user-search-field").on("keyup", function() {
    let input = $("#user-search-field").val();
    $.ajax({
      type: "GET",
      url: "/users",
      data: { keyword: input },
      dataType: "json"
    })
  });
});

5.入力値を曖昧検索する

users_controller.rb
def index
  return nil if params[:keyword] == ""
  @users = User.where(['name LIKE ?', "%#{params[:keyword]}%"] ).where.not(id: current_user.id).limit(10)
  respond_to do |format|
    format.html
    format.json
  end
end

params[:keyword]に値が入っていればそのまま処理は続けられ、空だった場合はそこで処理が終わります。
検索処理の内容は、whereメソッドを使用し、入力された値を含むかつ、ログインしているユーザーのidは除外するという条件で取得しています。

6.非同期通信の結果を得て、HTMLを作成

users.js
$(function() {
  function addUser(user) {
    let html = `
      <div class="chat-group-user clearfix">
        <p class="chat-group-user__name">${user.name}</p>
        <div class="user-search-add chat-group-user__btn chat-group-user__btn--add" data-user-id="${user.id}" data-user-name="${user.name}">追加</div>
      </div>
    `;
    $("#user-search-result").append(html);
  }

  function addNoUser() {
    let html = `
      <div class="chat-group-user clearfix">
        <p class="chat-group-user__name">ユーザーが見つかりません</p>
      </div>
    `;
    $("#user-search-result").append(html);
  }
  $("#user-search-field").on("keyup", function() {
    let input = $("#user-search-field").val();
    $.ajax({
      type: "GET",
      url: "/users",
      data: { keyword: input },
      dataType: "json"
    })
      .done(function(users) {
        $("#user-search-result").empty();

        if (users.length !== 0) {
          users.forEach(function(user) {
            addUser(user);
          });
        } else if (input.length == 0) {
          return false;
        } else {
          addNoUser();
        }
      })
      .fail(function() {
        alert("通信エラーです。ユーザーが表示できません。");
      });
  });
});

メンバーの追加・削除機能の実装

1.追加ボタンが押された時にイベントが発火するようにする

users.js
$(document).on("click", ".chat-group-user__btn--add", function() {
  • $(document).onすることで常に最新のHTMLの情報を取得する

2.追加ボタンをクリックされたユーザーの名前を、チャットメンバーの部分に追加し、検索結果からは消す

まずは検索結果から名前を消す方法

users.js
$(document).on("click", ".chat-group-user__btn--add", function() {
    console.log
    const userName = $(this).attr("data-user-name");
    const userId = $(this).attr("data-user-id");
    $(this)
      .parent()
      .remove();
  • 「追加」ボタンがクリックされたユーザーが、検索結果一覧から消えるどのユーザーのhtmlかを特定するためにdata-user-iddata-user-nameを取得するため、対象であるユーザー情報を定数へ代入。
  • イベントが発生した要素を取得し、その親要素を削除
  • 今イベントが発生している追加ボタンのaタグを起点に、その親要素のchat-group-userを削除

次にメンバーをチャットメンバーに追加する

users.js
$(document).on("click", ".chat-group-user__btn--add", function() {
    console.log
    const userName = $(this).attr("data-user-name");
    const userId = $(this).attr("data-user-id");
    $(this)
      .parent()
      .remove();
    addDeleteUser(userName, userId);
    addMember(userId);
  });
users.js(関数定義)
  function addDeleteUser(name, id) {   //画面上の追加
    let html = `
    <div class="chat-group-user clearfix" id="${id}">
      <p class="chat-group-user__name">${name}</p>
      <div class="user-search-remove chat-group-user__btn chat-group-user__btn--remove js-remove-btn" data-user-id="${id}" data-user-name="${name}">削除</div>
    </div>`;
    $(".js-add-user").append(html);
  }
  function addMember(userId) {        //DB上への追加
    let html = `<input value="${userId}" name="group[user_ids][]" type="hidden" id="group_user_ids_${userId}" />`;
    $(`#${userId}`).append(html);
  }

  • <input name='group[user_ids][]' type='hidden' value='ユーザーのid'> この記述により、userがDBに保存される

3.削除ボタンで、チャットメンバーから削除する機能を実装

users.js
$(document).on("click", ".chat-group-user__btn--remove", function() {
    $(this)
      .parent()
      .remove();
  });
  • .chat-group-user__btn--removeがクリックされたら、その親要素を削除。

4.ログイン中のユーザー(current_user)をチャットメンバーに表示し、他のメンバーを同じく表示する。

_form.html.haml
#chat-group-users.js-add-user
 .chat-group-user.clearfix.js-chat-member
   %input{name: "group[user_ids][]", type: "hidden", value: current_user.id}
   %p.chat-group-user__name= current_user.name
//current_user.idをgroup[user_ids]の配列に追加して、cuurent_user.nameを画面に表示

- group.users.each do |user|
  - if current_user.name != user.name
    .chat-group-user.clearfix.js-chat-member
      %input{name: "group[user_ids][]", type: "hidden", value: user.id}
      %p.chat-group-user__name
        = user.name
      %a.user-search-remove.chat-group-user__btn.chat-group-user__btn--remove.js-remove-btn
        削除
//user.nameと削除ボタンを全て表示

全体像

users.js
$(function() {
  var search_list = $("#user-search-result");

  function appendUser(user) {
    var html = `
              <div class="chat-group-user clearfix">
                <p class="chat-group-user__name">${user.name}</p>
                <div class="user-search-add chat-group-user__btn chat-group-user__btn-add" data-user-id="${user.id}" data-user-name="${user.name}">追加</div>
              </div>
              `;
    search_list.append(html);
  }
  function appendErrMsgToHTML() {
    var html = `
              <div class="chat-group-user clearfix">
                <p class="chat-group-user__name">ユーザーが見つかりません</p>
              </div>`;
    search_list.append(html);
  }
  function addDeleteUser(name, id) {
    let html = `
                <div class="chat-group-user clearfix" id="${id}">
                  <p class="chat-group-user__name">${name}</p>
                  <div class="user-search-remove chat-group-user__btn chat-group-user__btn--remove js-remove-btn" data-user-id="${id}" data-user-name="${name}">削除</div>
                </div>
                `;
    $(".js-add-user").append(html);
  }
  function addMember(userId) {
    let html = `<input value="${userId}" name="group[user_ids][]" type="hidden" id="group_user_ids_${userId}" />`;
    $(`#${userId}`).append(html);
  }
  $(".chat-group-form__input").on("keyup", function(){
    var input = $(".chat-group-form__input").val();
    $.ajax({
      type: 'GET',
      url: '/users',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(users) {
      search_list.empty();
      if (users.length !== 0) {
        users.forEach(function(user) {
          appendUser(user);
        });
      } else if (input.length == 0) {
        return false;
      } else {
        appendErrMsgToHTML();
      }
    })
    .fail(function() {
      alert('ユーザーが表示できません。');
    });
  });
  $(document).on('click','.chat-group-user__btn-add',function(){
    console.log
    const userName = $(this).attr("data-user-name");
    const userId = $(this).attr("data-user-id");
    $(this)
      .parent()
      .remove();
    addDeleteUser(userName, userId);
    addMember(userId);
  });
  $(document).on('click', '.chat-group-user__btn--remove', function(){
    $(this)
      .parent()
      .remove();
  });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails Webpacker】Webpacker::MissingEntryErrorのエラーはNode.jsのバージョンに起因するかもしれない話

【Rails Webpacker】Webpacker::MissingEntryErrorのエラーはNode.jsのバージョンに起因するかもしれない話

結論から

Nodeのバージョンを13.7以上にしてあげると
app>public>packsが生成され、当該エラーが解決される

解決までのプロセス

普通にrails new appnameでアプリを作成して
コントローラーやモデルの作成を行った後に、ブラウザでアプリをみてみると
このようなエラーが出る時がある。
スクリーンショット 2020-06-27 18.50.26.png

多数のサイトを見て、以下のコマンドを打ってみてもうまくいかない。

terminal
yarn
yarn install
yarn upgrade
brew install yarn
rails install yarn
rails webpacker:install

そして、webpackerのドキュメント READMEを読んでみると以下のコマンドが必要だとのことで、打ってみる

terminal
# こちらの一行はGemfileに書く
gem 'webpacker', '~> 5.x'

gem 'webpacker', git: 'https://github.com/rails/webpacker.git'
yarn add https://github.com/rails/webpacker.git
yarn add core-js regenerator-runtime

このyarn add ~をしたときに、以下のようなエラーが出た。

terminal
error browserslist@4.12.2: The engine “node” is incompatible with this module. Expected version “^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7". Got “13.5.0”

よってnodeのバージョンを13.7以上にしてみた。

まず
インストールできるNode.jsのバージョンを一覧で出力

terminal
nodebrew ls-remote

Node.jsの環境(ディレクトリ構成など)を自動的にセットアップしてくれる「setup」コマンドを実行しておきます

terminal
nodebrew setup

インストールできるNode.jsのバージョンを一覧で出力

terminal
nnodebrew install-binary v13.8.0

特定のバージョンの使用を命令

terminal
nodebrew use v13.8.0

以上によってnodeのバージョンを変えてあげると、当該エラーが消え、うまく画面が表示されるようになった!!

ターミナルを見てみると、

terminal
 Compiled all packs in /Users/nakayama_kazuhito/Desktop/node-version/public/packs

どうやらnodeが結構public直下にある、webpack関連のファイルの生成に関与しているそう。
んーとりあえず、一旦解決!!
またおって原因分析をします!
それではまた!!☺️

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

Rails のデータを React から参照するアプリケーション作成 ( Rails + React + MySQL )

はじめに

他の方の記事を参考に自分なりに Rails + React + MySQL の環境を作ってみたので、その時のやり方をまとめました。
上からコピペするだけで動くようにまとめてみたので、はじめてだけどやってみたいという方がいらっしゃれば、試していただきたいです。

コマンド

こちらの項目で実施することは以下となります。
上から順番にターミナルで実行していただければと思います。

  • やること
    • rails アプリケーションの作成
    • webpacker インストール
    • react インストール
    • MVC作成
    • migration
$ rails _5.2.4.2_ new react_sample_app --webpack=react -d mysql

$ cd react_sample_app

# Webpackを有効にする
$ rails webpacker:install

# Reactを有効にする
$ rails webpacker:install:react

# sample モデルを scaffold にて作成
$ rails g scaffold sample title:string body:string

# migrate
$ rails db:create
$ rails db:migrate

※routeなどの細かい設定が面倒だと思ってscaffoldを使いましたが、scaffoldが必須というわけではありません。

View

続いては、Viewの設定をしていきます。

application.html.erb

全体へ反映させたいことをこちらに記載していきます。

  • やること
    • javascript_include_tagjavascript_pack_tag へ変更する
    • view のファイル毎に読み込む javascript(React) を指定するために、<%= yield :javascript %>を追記
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>ReactSampleApp</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>

    <!-- javascript_include_tag を下記へ変更する -->
    <!-- javascript_include_tag 'application', 'data-turbolinks-track': 'reload' -->
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>

    <!-- view のファイル毎に読み込む javascript(React) を指定するために必要 -->
    <%= yield :javascript %>
  </body>
</html>

個別のerbファイル

ここでは例えとして、app/views/samples/index.html.erb上に変更を加えますが、他の画面で実装していただいも何も問題ありません。

  • やること
    • Rails から React へ渡すデータを作成(content_tagにより)
    • view のファイル毎に読み込む javascript(React) を指定する
app/views/samples/index.html.erb
<!-- 省略 -->
<!-- 一番下の行に以下を追記 -->

<!-- content_tag の data 属性を React へ渡す -->
<%= content_tag :div,
                id: "resources-container",
                data: {
                    q: params,
                    resources_path: samples_path,
                }.to_json do %>
<% end %>

<!-- view のファイル毎に読み込む javascript(React) を指定することができる -->
<% content_for :javascript do %>
  <%= javascript_pack_tag 'hello_react' %>
<% end %>

javascript

ここでは、コマンド入力時に作成したapp/javascript/packs/hello_react.jsxを用いますが、個々にファイルを設定していただいて問題ありません。

  • やること
    • Hello コンポーネントの作成
      • props を state に格納する
      • state.name を更新する関数を作成する
      • レンダリングする HTML要素を作成する
    • Rails から読み込みたい & React からレンダリングしたい要素を指定
    • 指定した要素(node)から、data を取得する
    • Hello コンポーネントを呼び出し
      • data を Hello コンポーネントの props として渡す
      • node へ React からレンダリングする
app/javascript/packs/hello_react.jsx
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'

// Hello コンポーネントの作成
class Hello extends React.Component {

  constructor(props) {
    super(props)

    // props を state に格納する
    this.state = {
      q: this.props.q || '',
      resources_path: this.props.resources_path || '',
      name: this.props.name || 'David'
    }
  }

  render() {
    // state.name を更新する関数を作成する
    const setName = e => {
      this.setState({
        name: e.target.value
      })
    }

    // レンダリングする HTML要素を作成する
    return (
        <div>
          <div>Hello {this.state.name}!</div>
          <input type="text" defaultValue={this.state.name} onChange={setName}/>
        </div>
    )
  }
}

document.addEventListener('DOMContentLoaded', () => {
  // Rails から読み込みたい & React からレンダリングしたい要素を指定する
  const node = document.getElementById('resources-container')
  // 指定した要素(node)から、data を取得する
  const data = JSON.parse(node.getAttribute('data'))

  ReactDOM.render(
    // Hello コンポーネントを呼び出し
    // Rails から取得した data を Hello コンポーネントの props として渡す 
    <Hello {...data}/>,
    // node へ React からレンダリングする
    node
  )
})

rails s と webpacker 起動を一括して実行する方法

以下に、scriptsの部分を追記します。
すると、yarn startをターミナル上で実行するたけで、rails s & bin/webpack-dev-serverを実行してくれます。

package.json
{
  "name": "react_sample_app",
  "private": true,
  "dependencies": {
    "@babel/preset-react": "^7.10.1",
    "@rails/webpacker": "5.1.1",
    "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
    "prop-types": "^15.7.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "devDependencies": {
    "webpack-dev-server": "^3.11.0"
  },

  // 以下を追記する
  "scripts": {
    "start": "rails s & bin/webpack-dev-server"
  }
}

$ yarn start

まとめ

いかがでしたか。
間違いなどがあれば、お手数をおかけしますがご指摘いただけると嬉しいです。

React を理解しているとスマホアプリを作れる React Native への導線が引けるかなーと思い勉強中です。
まずは、Rails と連携していけているアプリケーションを作れるようになりたいなーと思っています。

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

Rails+vue+Parcel環境でSFCのスタイルが効かなかった件

単一コンポーネントで定義したスタイルが反映されない件

Rails+vue+Parcel環境で開発をやっていた時vueのSFCで書いたスタイルが適用されない
いろいろ調べても記事はあまりなかった。
コードをしっかり確認したら解決したのでまとめてみました。

解決法

CSSを読み込むヘルパーメソッドを自作する

app/helpers/application_helper.rb
module ApplicationHelper
    ・・・

    def javascript_pack_tag(name)
        javascript_include_tag(manifest["#{name}.js"])
    end

+   def stylesheet_pack_tag(name)
+       stylesheet_link_tag(manifest["#{name}.vue"])
+   end

    ・・・

あとは読み込みたいスタイルがあるvueファイルを下記のように指定するだけ。

app/views/static_pages/home.html.erb
<% if logged_in? %>
・・・

      <div id="app"></div>
      <%= javascript_pack_tag 'index' %>
      <%= stylesheet_pack_tag '読み込みたいSFCの名前' %>

・・・

なぜこれで解決するの?

npm run watchでやっている内容は以下の通り
parcel watch app/javascripts/packs/index.html -d public/packs --public-url /packs/ --hmr-port 50000
トランスパイルしたものはpublic/packsに入ってくる。
ここで作成されるmanifestファイルの内容は以下の通り。


{
  "index.html": "/packs/index.html",
  "index.js": "/packs/packs.e31bb0bc.js",
  "FeedList.vue": "/packs/packs.e31bb0bc.css",
  "FeedItem.vue": "/packs/packs.e31bb0bc.css"
}

この内容をさっきのヘルパーでは読み込んでいる。

app/helpers/application_helper.rb
module ApplicationHelper
・・・

  private

    def manifest
      @manifest ||= load
    end

    def load
      manifest_path = Rails.root.join('public', 'packs', 'parcel-manifest.json')
      if manifest_path.exist?
        JSON.parse manifest_path.read
      else
        {}
      end
    end
end

これでマニフェストに書かれていることが読み込まれるので、先ほど作ったヘルパーメソッド(stylesheet_pack_tag)内でstylesheet_link_tagを使うことでrailsにCSSを読み込む!

後書き

ドキュメントに沿ってコピペだけしてた部分でこうした問題があったので、しっかりコードをよもうと思いました。

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

deviseのルーティングをカスタマイズする

はじめに

deviseは自動的にもろもろの設定を追加してくれる便利なGemですが、ルーティングをスタマイズすることになって苦戦したので、調べたことをまとめます。

deviseはさまざまなコントローラがありますが、基本的なDevise::RegistrationsControlleコントローラーとそのルーティングに絞ってをみていきます。

カスタマイズしない場合

デフォルトのルーティングはこの様になっています。

config/routes.rb
Rails.application.routes.draw do
  devise_for :customers, path: 'auth'
end
$ rails routes | grep registrations
cancel_customer_registration GET    /customers/cancel(.:format)   devise/registrations#cancel
  new_customer_registration GET    /customers/sign_up(.:format)   devise/registrations#new
  edit_customer_registration GET    /customers/edit(.:format)      devise/registrations#edit
      customer_registration PATCH  /customers(.:format)           devise/registrations#update
                            PUT    /customers(.:format)           devise/registrations#update
                            DELETE /customers(.:format)           devise/registrations#destroy
                            POST   /customers(.:format)           devise/registrations#create

pathオプションでリソースのパスを変更する

pathオプションを追加してauthと指定します。
こうすると、パスのリソースに相当するcurstomersauthへとが変わります。

config/routes.rb
Rails.application.routes.draw do
  devise_for :customers, path: 'auth'
end
$ rails routes | grep registrations
cancel_customer_registration GET    /auth/cancel(.:format)    devise/registrations#cancel
  new_customer_registration GET    /auth/sign_up(.:format)   devise/registrations#new
  edit_customer_registration GET    /auth/edit(.:format)      devise/registrations#edit
      customer_registration PATCH  /auth(.:format)           devise/registrations#update
                            PUT    /auth(.:format)           devise/registrations#update
                            DELETE /auth(.:format)           devise/registrations#destroy
                            POST   /auth(.:format)           devise/registrations#create

path_namesオプションでパスを追加する

path_namesオプションを追加してregistrationregisterを指定ます。
こうすると、registerのパスの階層が追加されます。

Rails.application.routes.draw do
  devise_for :customers, path_names: { registration: 'register' }
end
$ rails routes | grep registrations
cancel_customer_registration GET    /customers/register/cancel(.:format)   devise/registrations#cancel
  new_customer_registration GET    /customers/register/sign_up(.:format)  devise/registrations#new
  edit_customer_registration GET    /customers/register/edit(.:format)     devise/registrations#edit
      customer_registration PATCH  /customers/register(.:format)          devise/registrations#update
                            PUT    /customers/register(.:format)          devise/registrations#update
                            DELETE /customers/register(.:format)          devise/registrations#destroy
                            POST   /customers/register(.:format)          devise/registrations#create

skipオプションで対応付けをスキップする

skipオプションを追加してregistrationsを指定します。
こうすると、registratiosnsマッピングがスキップされます。
スキップされたままだと利用できないので、次の項で任意にカスタマイズする方法を説明します。

Rails.application.routes.draw do
  devise_for :customers, skip: 'registrations'
end
$ rails routes | grep registrations
## 何も出力されない

devise_scopeメソッドでパスを任意に指定する

devise_scopeメソッドを使うことでより柔軟にルーティングをカスタマイズできます。
ここでは、パスにsignupを指定しdeviseのコントローラーを追加します。

Rails.application.routes.draw do
  devise_for :customers, skip: 'registrations'
  devise_scope :customer do
    get 'signup', to: 'devise/registrations#new', as: :new_customer_registration
    post 'signup', to: 'devise/registrations#create', as: :customer_registration
  end
end
$ rails routes | grep registrations
new_customer_registration GET    /signup(.:format)   devise/registrations#new
    customer_registration POST   /signup(.:format)   devise/registrations#create

参考

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

Rails Tutorial 第5章完了

2020/6/16 1時間

夕食後にやったのですが、途中で寝てしまいました。
ほとんど進まなかったです。
仕事をした後の疲れた脳では辛いです。

2020/6/17 0.5時間

夜は無理だと思い、朝やることにしました。
仕事の前の30分だけしか時間がとれませんが、脳はフレッシュなので、夜より進みました。

2020/6/18 0.5時間

やる時間を夜から朝に変えてから、続けてできるようになりました。

2020/6/19 0.5時間

5.1.2まで終わりました。朝の30分です。

2020/6/20 2時間

会社がない土曜なので、大分進められました。5.2まで終わりました。

2020/6/21 1.5時間

会社がない日曜で、5.3まで終わりました。

2020/6/22 0.5時間

仕事の前の朝30分で、5.4まで終わりました。

2020/6/23 0.5時間

仕事の前の朝30分で、5章を終わりました。

これで5章を完了しました。
所要時間は7時間です。

感想ですが、仕事を終わった夜にやるのは脳が疲れていて寝てしまって無理だったので、朝に変えたのが効果がありました。

毎日30分やることで、前回から思い出す時間が省けました。

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

Rails:パンくずリストに2ページ目以降に「Xページ」と表示する方法

ページネーション機能がある一覧表示ページのパンくずリストに、「Xページ目」と表示するための実装方法を紹介します。

テキストだとちょっと伝わりづらいですが、

  • 1ページ目ならばページ名
    ScreenShot 2020-06-28 15.19.19.jpg

  • 2ページ目ならばページ名 > #{ページ数}ページ目
    ScreenShot 2020-06-28 15.16.04.jpg

のように、「1ページ目と2ページ目以降でことなる表示」にし、「2ページ目以降にはページ数を表示する」、ということを今回の仕様とします。

環境

自作のプロテイン口コミサービスに導入( https://github.com/yuki0920/supplebox )のアイテム一覧ページ(/products/index)に導入すること前提に説明します。

また、ページネーション機能はkaminariで、パンくずリスト機能はgretelですでに実装されていることを想定しています。

# Gemfile
gem 'kaminari'
gem 'gretel'

現状

1ページ目でも2ページ目以降でもページ名(アイテム一覧)が表示されています。

ScreenShot 2020-06-28 15.19.19.jpg

gretelのオーソドックスな使い方をしています。

config/breadcrumbs.rb
crumb :products do
  link 'アイテム一覧', products_path
  parent :root
end
app/views/products/index.html.haml
- breadcrumb :products

Xページ目を表示する

設定ファイルに追記します。parentにproductsを設定します。ページ数をpageとして第2引数で受け取り、
pageを使ってページ数を表示するようにします。

第2引数で受け取った値を表示できる、というのはREADMEのサンプル集に載っています。

config/breadcrumbs.rb
crumb :products do
  link 'アイテム一覧', products_path
  parent :root
end

# 追記
crumb :products_pagination do |page|
  link "#{page}ページ目"
  parent :products
end
app/views/products/index.html.haml
- breadcrumb :products_pagination params[:page]

呼び出し元のViewテンプレートでは、breadcrumbメソッドの引数に注目です。

  • 第1引数に、products_pagenation
  • 第2引数に、params[:page]

kaminariでは、ページ数がクエリパラメータとして渡ってきます(例えば、/products?page=2)ので、params[:page]でページ数を取得して第2引数として渡すのです。

このようにすることで、1ページ目も2ページ目以降もパンくずリストにページ数を表示することができました。

が、1ページ目のときには問題があります。

params[:page]がnilのため、Home › アイテム一覧 › ページ目の表示になってしまいます。

1ページ目と2ページ目以降で場合分けをする

ということで、param[:page]の値を使って、1ページ目と2ページ目以降で場合分けをします。

app/views/products/index.html.haml
- if params[:page].nil? || params[:page] == 1
  = breadcrumb :products
- else
  = breadcrumb :products_pagination, params[:page]

これで、当初の目標を満たすことができました。

  • 1ページ目ならばページ名
  • 2ページ目ならばページ名 > #{ページ数}ページ目

とはいえ、Viewファイルがごちゃっとしているのが気になります。

リファクタリング

ロジックをViewファイルに書くと見通しが悪くなってしまうので、メソッド化してヘルパーに切り出します。

app/helpers/application_helper.rb
def breadcrumb_pagination
  if params[:page].nil? || params[:page] == 1
    breadcrumb :products
  else
    breadcrumb :products_pagination, params[:page]
  end
end

呼び出し元もヘルパーに定義したメソッドに修正します。
以上で完了です。

app/views/products/index.html.haml
- breadcrumb_pagination

補足(System Spec)

パンくずリストの実装に関して下記のようなテストを書いて仕様を担保しました。
パンくずリストに関わる実装のみを抜粋しているので、雰囲気だけでも伝えられれば。

やはり、テストを書くとリファクタリングがはかどりますね!

spec/system/products_spec.rb
# frozen_string_literal: true

require 'rails_helper'

describe 'アイテム', type: :system do
  describe '一覧機能' do
    before do
      create_list(:product, 13)
    end

    it 'ユーザーはアイテム一覧を閲覧できること' do
      visit products_path
      expect(page).to have_content 'Home › アイテム一覧'
      expect(page).to_not have_content 'Home › アイテム一覧 › 1ページ目'
      expect(page).to have_selector '.pagination'
      within '.pagination' do
        click_link '2'
      end
      expect(page).to have_content 'Home › アイテム一覧 › 2ページ目'
      within '.breadcrumbs' do
        expect(page).to have_link 'アイテム一覧' ,href: products_path
      end
    end
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ruby on rails : gem install sqlite3 のコマンドが通らない

昨日(2020/6/27)からrailsの勉強をしているのですが、
gem install sqlite3  のコマンドでエラーが起きて困っております・・。

↓エラー文
ERROR: While executing gem ... (Gem::FilePermissionError)
You don't have write permissions for the /Library/Ruby/Gems/2.6.0 directory.

下記参考動画を真似して環境構築したのですが(←どこかでおかしなことしてしまった?)、うまく環境変数の設定ができておらず、echo $PATH で変数の中身を確認してみると、
/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
と、なっていて、環境変数が上手く設定できていません。

動画のように環境構築進めたのですが、途中でnode.jsをインストールしなさい、的なエラーが出たりで、
何度か環境変数の設定
echo 'eval "$(rbenv init -)"' > ~/.bash_profile
source ~/.bash_profile
を打ち込んだのを覚えています。原因としては.bash_profileの
/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin と2重になっているのを
/usr/local/bin:/usr/local/sbin
に変更できたらgem install sqlite3 のコマンドが通るのかなと思ったのですが、いかがでしょうか?
詳しい方がいらっしゃいましたら、ご教示いただけますと幸いです。
※足りない情報がありましたら申し訳ございません。。

念のため 各バージョンも明記しておきます。

ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]
Rails 6.0.3.2
rbenv 1.1.2
Homebrew 2.4.2

▼参考動画
https://www.youtube.com/watch?v=OHycvUQ4VNQ&t=1431s

【追記】
railsのサーバーを立ち上げたりなどはできております。
※ただ、rails s だと localhost:3000に繋げないので、
rails s -b 0.0.0.0 でつないでいます。

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

http://localhost:3000/rails/infoとは

にブラウザからアクセスするとルーティングの一覧が表示される。

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

【Redis】” SCAN ” による安全なKey取得を Rails で実装する

この記事ではSCANによるKey取得をRailsで実装する例」を紹介します。

Key取得の方法にはKEYSSCANがありますが、「サーバ稼働の安全性」を考慮するとKey取得にはSCANを使うことが推奨されています。詳しくは先日僕が投稿した「【Redis】KEYSによるKey取得の危険性とSCANによる安全な対処」をご覧ください。

SCANでのKeyを取得」を実装して安全にキャッシュデータの削除や更新を行えるようにしましょう。

実装例

app/models/concerns/cache_util.rb
module CacheUtil
  def scan_cache_keys(pattern)
    redis = Redis.current
    cursor = '0'
    keys = []
    loop do
      cache_data = redis.scan(cursor, match: pattern)
      cursor = cache_data.first        # カーソルを更新
      keys.concat cache_data.last      # パターンマッチしたKeyを追加
      return keys if cursor == '0'
    end
  end
end

処理は単純で、「カーソルが”0”になるまで(探索ループが終わるまで)、パターンマッチするKeyを探し、”0”になったらKeyのデータを返り値として送る」というものです。

どのモデルファイルからも呼び出せるようにしたいので、app/models/concerns/内でモジュールとして定義します。

7行目のscanメソッドは返り値として以下のような、[次のカーソル値, [Keyを格納する配列]]を返します。

["3", [key_example01, key_example05, key_example02, key_example06]]

使用例

例えば「Userデータを更新した時に、Keyの名前に"example"を含むキャッシュを削除する」というケースの処理は以下の通りです。

app/models/user.rb
class User < ApplicationRecord
  include CacheUtil

  # some code

  after_update :delete_cache

  # some code

  def delete_cache
    redis = Redis.current
    keys = scan_cache_keys("*example*")
    redis.del(keys) if keys.present?
  end
end

終わり

カーソルという概念を理解できると意外とすぐに実装できるかと思います。今後Redis関連のissueがあるかは分かりませんが、Redisには多くのコマンドがあるみたいなので勉強しておこうと思います。

質問・ご指摘はコメント欄にお願いします。

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

Rspec 架空モデルのアソシエーション

投稿背景

架空モデルが関係する部分でテストを実装していたところ、架空モデルの定義でエラーが解決できずにいて手こずっていたが、なんとか解決できたので備忘録として投稿

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

chat_notice.rb
belongs_to :guide_visitor, class_name: 'Guide', foreign_key: 'visitor_id', optional: true
belongs_to :guide_visited, class_name: 'Guide', foreign_key: 'visited_id', optional: true
belongs_to :tourist_visitor, class_name: 'Tourist', foreign_key: 'visitor_id', optional: true
belongs_to :tourist_visited, class_name: 'Tourist', foreign_key: 'visited_id', optional: true
tourist.rb
has_many :tourist_active_notices, class_name: 'ChatNotice', foreign_key: 'visitor_id', dependent: :destroy
has_many :tourist_passive_notices, class_name: 'ChatNotice', foreign_key: 'visited_id', dependent: :destroy
guide.rb
has_many :guide_active_notices, class_name: 'ChatNotice', foreign_key: 'visitor_id', dependent: :destroy
has_many :guide_passive_notices, class_name: 'ChatNotice', foreign_key: 'visited_id', dependent: :destroy

factory

chat_notice.rb
FactoryBot.define do
    factory :chat_notice do
        association :chat
        association :guide_visitor
        association :guide_visited
        association :tourist_visitor
        association :tourist_visited
    end
end

実行したテスト

chat_notice.rb
require 'rails_helper'

RSpec.describe ChatNotice, type: :model do
    let(:chat) { create(:chat) }
    let(:room) { create(:room) }
    let(:guide_visitor) { create(:guide) }
    let(:guide_visited) { create(:guide) }
    let(:tourist_visitor) { create(:tourist) }
    let(:tourist_visited) { create(:tourist) }
    let!(:chat_notice) { build(:chat_notice, chat_id: chat.id, visited_id: tourist_visited.id, visitor_id: guide_visitor.id) }


    describe 'ChatNotice保存テスト' do
        context 'チャット通知が正しく保存される' do
            it '全て入力されているので保存' do
                expect(chat_notice).to be_valid
            end
        end
    end

    describe 'ChatNoticeアソシエーションのテスト' do
        context 'Tourist_visitorモデルとの関係' do
            it 'N:1となっている' do
                expect(ChatNotice.reflect_on_association(:tourist_visitor).macro).to eq :belongs_to
            end
        end
        context 'Tourist_visitedモデルとの関係' do
            it 'N:1となっている' do
                expect(ChatNotice.reflect_on_association(:tourist_visited).macro).to eq :belongs_to
            end
        end
        context 'Guide_visitorモデルとの関係' do
            it 'N:1となっている' do
                expect(ChatNotice.reflect_on_association(:guide_visitor).macro).to eq :belongs_to
            end
        end
        context 'Guide_visitedモデルとの関係' do
            it 'N:1となっている' do
                expect(ChatNotice.reflect_on_association(:guide_visited).macro).to eq :belongs_to
            end
        end
    end
end

 エラー内容

こんな感じでキーが見つからないよーっていうエラーが出る。

# KeyError:
     #   key not found: "guide_visitor"

guide_visitorはguideモデルを基に定義された架空のモデルだから、factoryにguideを定義していればテストは通ると思ってたけど、失敗。

 原因

factory の定義の中で association を定義するということはその同名の factory が存在すること前提になってるみたいで、guide_visitorをfactoryに定義してなかったからエラーが発生してた。

 解決方法

factory の association 定義に対して、それが利用することになる association 先の factory の名前を別のものにしたい場合にはfactory:キーワード引数を指定してやる

FactoryBot.define do
    factory :chat_notice do
        association :chat
        association :guide_visitor, factory: :guide  #factory: :guideを追加
        association :guide_visited, factory: :guide  #factory: :guideを追加
        association :tourist_visitor, factory: :tourist  #factory: :touristを追加
        association :tourist_visited, factory: :tourist  #factory: :touristを追加
        checked { Faker::Boolean.boolean }
    end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsの基本用語(中級編)

プログラミング初心者が
Railsの基本用語を定着させるためのアウトプット用の記述です。中級編になります。

Railsの基本用語

1.database.yml
Railsアプリケーションのデータベースの設定ファイル。
運用環境ごとに設定を変更することができる。

2.RubyGems
Rubyのライブラリを管理するシステム。

3.Gem
RubyGemsのライブラリひとつひとつのこと。

4.Gemfile
アプリケーションで使用するGemの「名前」と「バージョン」の情報を記載して、管理するファイル。

5.Gemfile.lock
bundle installによってインストール済みとなったGemの情報を記録するファイル。

6.7つのアクション
Railsにおいて慣習的に決められているアクションのこと。
index…一覧表示
show…詳細表示
new…生成
create…保存
edit…編集
update…更新
destroy…削除
resourcesメソッドで7つのアクションへのルーティングが自動生成できる。

7.devise
ユーザー管理機能を簡単に実装するためのGem。

8.user_signed_in?メソッド
ログインしているかどうかの判定を行うメソッド。

9.Railsの命名規則
クラス名…アッパーキャメルケース
メソッド名…スネークケース
変数名…スネークケース

10.configure_permitted_parametersメソッド
deviseが提供しているユーザー登録の際に使用できるメソッド。
ログイン時に送られてくるパラメータを制限するストロングパラメータは、deviseのGem内に記述されており編集できないため使用し許容する必要がある。

11.application_controller.rbファイル
共通のテンプレート、コントローラを書くファイル。
rails g controllerで生成したコントローラが予め継承しているファイル。

12.current_userメソッド
devise用の現在ログインしているユーザーの情報を取得するメソッド。

13.mergeメソッド
ハッシュを結合させるときに使用するメソッド。

14.pry-rails
Railsにおけるデバッグ用のGem。
Railsの処理を一時停止し、コンソールを起動することができるbinding.pryという機能が使えるようになる。

15.フラッシュメッセージ
アクションの実行後に簡単なメッセージを表示するRailsの機能。
flashオブジェクトを使う。

16.collection_check_boxes
ビューにチェックボックスの表示を行うためのヘルパーメソッド。

17.redirect_toとrender
実行するとビューが表示される。
redirect_toはコントローラを経由してビューが表示される。
renderはそのままビューが表示される。
元のインスタンス変数の値が上書きされるかどうかの違い。

18.form_withの仕様
引数の内容によってデータの送信先を推測している。
空のデータならcreate、既存のデータならupdate。

19.errorsメソッド
失敗をすると変数にエラーメッセージが格納される。
エラーメッセージの取得ができるメソッド。

まとめ

Rails(中級編)の基本用語をまとめました。

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

railsでFailed to open TCP connection to oauth2.googleapis.com:443 (getaddrinfo: Name or service not known) というエラーがでた。

Failed to open TCP connection to oauth2.googleapis.com:443 (getaddrinfo: Name or service not known)というエラーがでた。

環境 VirtualBox vagrant (CentOS)

RailsでOminiauth使って、Googleログイン機能作って、Googleログインしようとした。すると

Failed to open TCP connection to oauth2.googleapis.com:443 (getaddrinfo: Name or service not known)

というエラーが。

解決策

vagrant reloadをすることで治った。
どうやら、途中で、PCをスリープにすると、CentOSのネットワークに障害が起きてしまうようだった。
その場合はvagrant reloadしなくてはならない。

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

rails カラムの追加

初投稿です。
アウトプットがてら記事を投稿します。
記事を投稿する経緯は自分が忘れてしまうからです。
今回は新しくカラムを追加したり、削除する際の方法を記載いたします。

  1. カラムの追加
  2. カラムの削除

【1. カラムの追加】
テンプレートは下記です。

 rails g migration Addカラム名Toテーブル名 カラム名:型名

実際にテーブルにカラムを追加する際の一例を記載いたします
ターミナルを開き下記のものを記述いたします。

rails g migration AddNameToLists name:string

Lists(テーブル)にstring型のNameカラムを追加することになります。
最後は下記で更新。

rails db:migrate

これで\db\migrateに新しいマグレーションファイルが格納されているはずです。

【2. カラムの削除】
テンプレートは下記です。

 rails g migration Removeカラム名Fromテーブル名 カラム名:型名

実際にカラムを削除する際の一例を記載いたします
ターミナルを開き下記のものを記述いたします。

 rails g migration RemoveNameFromLists name:string

Lists(テーブル)からstring型のNameカラムを削除します。
最後は下記で更新。

rails db:migrate

これで\db\migrateから消えてるはずです。

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

Ruby on Railsの基本用語

プログラミング初心者が
Ruby on Railsの基本用語をアウトプット用に記述していきます。

Ruby on Railsの基本用語

1.Ruby on Rails(Rails)
Rubyの主要フレームワーク。
コードの記述量が短くシンプルで、素早くWebアプリケーションが作れる。
スピードが求められる開発や、プロトタイプの作成などに使われる。

2.railsコマンド
Railsに関する命令をするためのコマンド。

3.Sequel Pro(シークエル・プロ)
データベースの中身をわかりやすく視覚化して表示するアプリケーション。
データベースをGUIで操作することができ、より簡単に管理できる。

4.MVC
Webアプリケーションシステムの処理の構造を表す言葉。
モデル/ビュー/コントローラの略称。

5.ルーティング
クライアントからのリクエストの行き先を定義する。

6.コントローラ
リクエストに対応する処理をまとめて用意しておき、ルーティングからリクエストを受け取って処理を行なった後、クライアントにレスポンスを返す役割。
レスポンスに必要となるデータがあれば、他の役割と連携してデータを取得したり受け渡しを行い、レスポンスを完成させる。

7.アクション
コントローラ内における処理の種類のこと。

8.ビュー
ブラウザにレスポンスとして返す見た目を設定する役割。

9.ERB
HTMLにRubyの記述を埋めることができるテンプレートエンジンのこと。
拡張子は「.html.erb」。
Rubyの記述は<%= %>で囲む。

10.テーブル
データベース内のデータを表形式で収納する場所のこと。
行をレコード、列をカラムという。
データベース自体にはそのままデータを保存できないため必要。

11.モデル
データベースへのアクセスなど情報のやりとりに関する処理をする役割。
テーブルとモデルはそれぞれ対応している。

12.マイグレーション
テーブルの設計図・仕様書のこと。
マイグレーションファイル内にどのようなテーブルを作成するか記述する。

13.カラムの型
カラムにどんな種類のデータが入るのかを示すもの。

14.コンソール
ターミナルからアプリケーションの操作を行う仕組み。(Railsに限らず)
Railsではrails cコマンドで実行することができる。

15.ActiveRecordメソッド
モデルがテーブル操作に関して使用できるメソッドの総称。
テーブルに情報を保存したり取得するために使用する。

16.ヘルパーメソッド
Railsで用意されたHTMLの作業を簡単にするためのメソッド。
主にビューでHTMLタグを出現させたりテキストを加工したりするために使う。
記述がシンプルになる。セキュリティ上の問題を解消する。

17.パラメータ
リクエストに含まれてサーバの外部から渡されるデータのこと。
「URLに含めるもの」や「フォームから送信されるもの」がある。

18.params
送られたきたパラメータをハッシュのような構造で格納したもの。
フォームで送信されたデータもparamsの中に格納されてコントローラで受け取られる。

まとめ

Railsに限った用語でないものも入っていますが、合わせて学習したのでこのまま記述しておきます。

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

日本語のRailsアプリケーションを作るときにしたほうがよさそうなこと

はじめに

仕事で作っているRailsアプリケーションが複数の言語を扱っています。その関係であらためてRailsガイドのRails 国際化 (i18n) APIを読み直したのですが…今までいくつか日本語のアプリケーションを書いてきましたが、もっとラクにできたのでは…という気持ちになりました。国際化と書かれると、どうせ日本語しか使わないしな…と思ってしまいますが、よくよく考えれば、Railsの標準の言語の英語でないもの言語用ということは…必然的に国際化なんですよね。

ですので、日本語のRailsアプリケーションを書くのならば、この章は必読だと思います。読んでない方はぜひ読んでみてください。きっとプログラムの記述量が減ると思います。

(おしまい)

…だと、自分がおもしろくないので、こんなことができたよ、という簡単なサンプルを作りました。ソースも公開していますので、気になる方は動かして遊んでみてください。

設定

国際化(i18n)の機能を使うので、設定をします。デフォルトでは置いていないので、新規作成します。(ソース
詳しくはガイドのI18nモジュールを設定するをみてください。

config/initializers/locale.rb
# config/locales以下の〜.ymlや〜.rbを訳語ファイルとして読み込みます。
I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
# デフォルトの使用言語を日本語にします。
I18n.default_locale = :ja

訳語ファイルのディレクトリ構成

config/locales/ja.ymlに全部書いても動くには動きますが、管理のしやすさから役割に応じてファイルを分割しておくほうがよいと思います。Railsガイドに例が出ていますが自分もそれが一番しっくりきました。

日本語しか扱わないのであれば、モデル名のディレクトリは作らず、そのままモデル名のファイルにしてもよいと思います。

config/locales/
├── defaults.yml
├── models
│   └── blog.yml
└── views
    └── defaults.yml

Railsのデフォルトのエラーメッセージや、日時などのフォーマットの日本語訳を手に入れる

Githubに有志による翻訳ファイル、rails-i18nのがありますので、それを感謝しながら利用します。

Gemfile
gem 'rails-i18n'

フォームの項目名や、エラーメッセージを日本語化

ちゃんと日本語化をしたい理由がこれだと思います。
項目名をあちこちに書いたりするのは大変ですよね…。
先に書いた設定をして、モデルや属性の訳語を登録すれば、こんなふうに表示されるようになります。

image.png

モデルの訳語

ここではBlogモデル訳語の例です。

config/locales/models/blog/ja.yml
ja:
  activerecord:
    models:
      blog: 記事
    attributes:
      blog:
        title: タイトル
        body: 本文
        user_id: 著者
        status: 状態
        created_at: 作成日時
        updated_at: 更新日時

エラーメッセージの表示

image.png

モデルに対してsavevalid?を行うと、errors.fullmessagesに項目名を含んだエラーメッセージが入っているので、それを表示しています。(ソース

app/views/shared/_form_error_messages.html.erb
  :
<% model.errors.full_messages.each do |message| %>
  <li><%= message %></li>
<% end %>
  :
<% end %>

モデルや属性の訳語の取得

フォーム

image.png

入力項目のラベルを#labelで翻訳された属性名で作れます。(ソース

フォーム以外

image.png

確認画面などで入力フォームの項目名と合わせるのに、手書きでがんばらなくても#human_attribute_name(属性名)で訳語を呼び出せます。(ソース

  :  
<thead>
  <tr>
    <th><%= Blog.human_attribute_name(:title) %></th>
    <th><%= Blog.human_attribute_name(:user_id) %></th>
    <th></th>
  </tr>
</thead>
  :

.model_name.humanで翻訳されたモデル名も取得できます。(…あんまり使いみちが思いつかず、コンソールですみません…。)
image.png

フォームオブジェクト

image.png

モデルじゃないけどモデルっぽいものや、複雑だったりするフォームを作るときにはActiveModelを使ったフォームオブジェクトを使いますが、同じようにできます。ただし、訳語ファイルの項目名がactive_recordではなくactive_modelになっています。(訳語のソースフォームのソース

config/locales/models/forms/session/ja.yml
ja:
  activemodel:
    models:
      forms/session: セッション
    attributes:
      forms/session:
        login_id: ログインID
        password: パスワード

日時表示などの日本語化

日時などを文字列にしようとするときに、つい今だけよければ…と思って、テンプレート内で#strftimeを使ってしまったりします…。こういうことを繰り返して、アプリケーション内でびみょーーーーーうな差異が散らばってしまって、あとで悶々としてしまうことが何度もありました。
I18n.lを使えば、もう少しまとまりを作ることができると思います。

image.png

上の例で書いてあるフォーマットは、rails-i18nのこのあたりに定義があります。
最後の:nengetsuはありませんが、これは訳語ファイルの中で定義したものです。
これらを使えば、多少バリエーションも固定化できると思います。(まだ成功体験はないです。)

config/locales/views/defaults/ja.yml
ja:
  date:
    formats:
      nengetsu: "%y年%m月"

おわりに

Railsガイドをあらためて読み直すと、実感できずにスルーしていた部分に気づけてよいな、と思いました。
用意されている便利な機能を使って、なるべくがんばらないようにしたいです。

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

gem 'rails-i18n', '~> 6.0.0'�を追加し、bundle install でエラーが出た時の解決方法

エラーの出現

Rails5でエラーメッセージの日本語化をするために
Gemfileに下記を追加

gem 'rails-i18n', '~> 6.0.0'

次に、ターミナルにて

$ bundle install

を実行したところ、下記のエラーが発生しインストールができない。

The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Bundler could not find compatible versions for gem "railties":
  In snapshot (Gemfile.lock):
    railties (= 5.2.4.3)

  In Gemfile:
    coffee-rails (~> 4.2) was resolved to 4.2.2, which depends on
      railties (>= 4.0.0)

    rails (~> 5.2.4, >= 5.2.4.3) was resolved to 5.2.4.3, which depends on
      railties (= 5.2.4.3)

    rails-i18n (~> 6.0.0) was resolved to 6.0.0, which depends on
      railties (>= 6.0.0, < 7)

    sass-rails (~> 5.0) was resolved to 5.1.0, which depends on
      railties (>= 5.2.0)

    web-console (>= 3.3.0) was resolved to 3.7.0, which depends on
      railties (>= 5.0)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

エラー原因

gem 'rails-i18n', '~> 6.0.0'

のバージョン指定が間違っていた。
上記バージョンはrails6の場合だった。

解決方法

今回のrailsのバージョンはrails5だったので、その場合は、

gem 'rails-i18n', '~> 5.1'

とすればOK!!

その後、

$ bundle install

を実行したら無事インストールできた。

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

rails フォーム 初期値

フォームのところに
<%= f.text_field :name%>

<%= f.text_field :name, value: "名無し" %>
value: で設定する。

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

RailsのSTIやenumで想定外の値が入らないようにする方法

まとめ

STIやenumに入りうる値をテーブルで持ち、外部キー制約を追加しましょう。

https://en.wikipedia.org/wiki/Reference_table

サンプルリポジトリ

https://github.com/hanachin/iikanji_enum

やり方

idの型とSTIのカラムやenumのカラムの型を一致させる。
外部キー制約をはる。

db/migrate/20200627151958_create_posts.rb
class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      t.string :type
      t.integer :state
      t.string :title
      t.text :body

      t.timestamps
    end

    create_table :post_states do |t|
      t.string :name

      t.timestamps
    end
    add_foreign_key :posts, :post_states, column: :state

    create_table :post_types, id: :string do |t|
      t.timestamps
    end
    add_foreign_key :posts, :post_types, column: :type
  end
end

こうなる

db/schema.rb
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `rails
# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2020_06_27_160353) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "post_states", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "post_types", id: :string, force: :cascade do |t|
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "posts", force: :cascade do |t|
    t.string "type"
    t.integer "state"
    t.string "title"
    t.text "body"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  add_foreign_key "posts", "post_states", column: "state"
  add_foreign_key "posts", "post_types", column: "type"
end

以下のような感じのSTI/enumを使ったクラス定義があるとき

app/models/post.rb
class Post < ApplicationRecord
  enum state: { draft: 0, published: 1 }
end
app/models/draft_post.rb
class DraftPost < Post
end
app/models/published_post.rb
class PublishedPost < Post
end

STI/enumのカラムがとりうる値のレコードを作成しておく

app/models/post/state.rb
class Post < ApplicationRecord
  class State < ApplicationRecord
    class << self
      def seed
        Post.states.each do |state, id|
          find_or_create_by!(id: id, name: state)
        end
      end
    end
  end
end
app/models/post/type.rb
class Post < ApplicationRecord
  class Type < ApplicationRecord
    class << self
      def seed
        [PublishedPost, DraftPost].each do |klass|
          find_or_create_by!(id: klass.name)
        end
      end
    end
  end
end

例えばtypeカラムに存在しないクラスの名前を入れたときちゃんとエラーになって保存できない

Loading development environment (Rails 6.0.3.2)
irb(main):001:0> post = Post.first
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT $1  [["LIMIT", 1]]
irb(main):002:0> post
=> #<DraftPost id: 3, type: "DraftPost", state: "draft", title: "test", body: "test", created_at: "2020-06-27 16:14:49", updated_at: "2020-06-27 16:14:49">
irb(main):003:0> post.type = "YavayPost"
irb(main):004:0> post.save!
   (0.3ms)  BEGIN
  DraftPost Update (1.2ms)  UPDATE "posts" SET "type" = $1, "updated_at" = $2 WHERE "posts"."id" = $3  [["type", "YavayPost"], ["updated_at", "2020-06-27 16:40:40.492136"], ["id", 3]]
   (0.2ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):4
ActiveRecord::InvalidForeignKey (PG::ForeignKeyViolation: ERROR:  insert or update on table "posts" violates foreign key constraint "fk_rails_43c128f7b9")
DETAIL:  Key (type)=(YavayPost) is not present in table "post_types".
irb(main):005:0>

またenumカラムも同様に存在しないstateを入れたときちゃんとエラーになって保存できない

irb(main):001:0> class Post; enum state: { amasawa: 4423 }; end
=> {:state=>{:amasawa=>4423}}
irb(main):002:0> post = Post.first
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT $1  [["LIMIT", 1]]
irb(main):003:0> post
=> #<DraftPost id: 3, type: "DraftPost", state: nil, title: "test", body: "test", created_at: "2020-06-27 16:14:49", updated_at: "2020-06-27 16:14:49">
irb(main):004:0> post.state = :amasawa
irb(main):005:0> post.save!
   (0.3ms)  BEGIN
  DraftPost Update (1.3ms)  UPDATE "posts" SET "state" = $1, "updated_at" = $2 WHERE "posts"."id" = $3  [["state", 4423], ["updated_at", "2020-06-27 16:43:03.201733"], ["id", 3]]
   (0.2ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):5
ActiveRecord::InvalidForeignKey (PG::ForeignKeyViolation: ERROR:  insert or update on table "posts" violates foreign key constraint "fk_rails_93ccb3c476")
DETAIL:  Key (state)=(4423) is not present in table "post_states".
irb(main):006:0>

まとめ

データベースはべんり。

一部のinclusionも同じ手法で実装できるのでぜひやってみてください。

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