- 投稿日:2020-06-28T23:32:21+09:00
[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_spec
やsystem_spec
は実行が重いので、テスト結果をすぐに知りたい場合は、ファイル or 行指定
する方が時短になりそうですね。
- 投稿日:2020-06-28T23:18:16+09:00
Railsのnokogiriでスクレイピングしてactiverecord-importで配列をDBに保存する
スクレイピングしてDBに保存したい人向けです
外部からデータ取ってくることに憧れてたので、初めてスクレイピングやった忘備録です。
初めてスクレイピングしたのでおてやわらかにお願いします笑何をスクレイピングしたの?
paypayのキャンペーンwebページから店舗名を取得しました!
スクレイピング前のDB
スクレイピング後のDB
環境
- 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.rbmodule 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.rbclass ShopsController < ApplicationController include PaypayScrapesConcern def new if params[:format] == "paypay" @shops = set_paypay_shops end @shop = Shop.new end endnew.html.slimh2 お店を追加する = render 'form' = link_to "paypayのキャンペーンを表示する", new_shop_path("paypay") - if @shops.present? - @shops.each do |shop| p = shop
localhost:3000/shop/new
↓↓↓↓↓↓
ボタンを押すと
localhost:3000/shop/new.paypay
↓↓↓↓↓↓
これでviewで確認できました。
保存用のactionを作成
まずはroutesにactionを追加
routes.rbresources :shops do collection do post 'paypay_save' end endcontrollerに新しくメソッド追加
shops_controller.rbdef paypay_save @shops = set_paypay_shops shops = [] @shops.each do |shop| shops << Shop.new(name: shop) end # DBアクセス一回で配列を保存できるgem activerecord-import Shop.import shops endviewも保存用のactionを使えるように設定
new.html.slimh2 お店を追加する = 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
- 投稿日:2020-06-28T23:00:11+09:00
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ガイド
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 endclass Book < ApplicationRecord (中略) has_and_belongs_to_many :users endUserと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
- 投稿日:2020-06-28T22:42:05+09:00
Docker + Rails で Bundler v2.X を使う
Gemfile.lock の
BUNDLED WITH
と Docker イメージ内の bundler バージョンに乖離があるとエラーになる。https://qiita.com/tanakaworld/items/e15ff9dbdd4b628378c2
次の通り対策ができる。
- イメージ内の gem をアップデート
gem update --system
- 任意のバージョンの bundler をインストール
gem install bundler -v <バージョン>
例:
DockerfileFROM 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
- 投稿日:2020-06-28T22:22:57+09:00
ほとんど意味のないコピペ記事はどこから来ているのか
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)
を併用しているのって、どこかで自分が書いた部分とコピペが混じっているからではないかと勘繰ってしまう。
- 投稿日:2020-06-28T21:40:02+09:00
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:editcredentials.ymlaws: 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(ここだけ画像です!)
aws_accses_key_id と aws_secret_accses_key がArgumentError??
どういうこと?
そういえば、carrierwaveでAWSのキーを使ってた。carrierwave.rbconfig.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.rbconfig.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.rbapp_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.rbapp_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/currentbundle exec unicorn_rails -c config/unicorn.rb -E production -Dを叩いてコケる
~/MyNote/releases/20200628222655/current
ってどこだよ(哲学)しょうがないので絶対パスにしてみる。
config/unicorn.rbapp_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.logbundler: 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' endconfig/deploy/production.rbserver '自身の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 endconfig/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' endconfig/database.ymlproduction: <<: *default database: 本番環境のDB名 username: root password: password socket: /var/lib/mysql/mysql.sock encoding: utf8EC2で【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=productionEC2で【sudo vim /etc/nginx/conf.d/rails.conf】を叩いて編集
rails.confupstream 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】を叩いてコケる。
- 投稿日:2020-06-28T20:34:34+09:00
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
モデル修正
- 投稿日:2020-06-28T20:34:34+09:00
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
モデル修正
- 投稿日:2020-06-28T20:14:58+09:00
【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 stopmodelを作成
$ 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 reconfirmableAPIのログイン機能作成の場合
※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
- 投稿日:2020-06-28T20:14:43+09:00
【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.rbclass 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.rbclass HomeController < ApplicationController def index end end◆ routes.rb編集
config/routes.rbRails.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.jsimport 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.rbclass 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.rbclass 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' %>参考文献
- 投稿日:2020-06-28T18:44:39+09:00
【Rails】フリマアプリ商品編集機能について(プレビュー編集・DB更新)
はじめに
草野と申します。
今回の投稿は、プログラミングスクールでチーム開発にて行ったフリーマーケット系ECサイトのクローンアプリ商品編集機能についてです。自分用のメモのため、文章は拙いですが、少しでも初学者の助けになればと考えています。
内容は、表題にもあるとおり、プレビュー編集とDB更新についてです。未熟な点も多いと思います。不備等ありましたらご指摘ください。随時改善して行こうと思います。ちなみに私は、スクール自体は卒業しており、学習した内容の振り返りとして投稿させて頂いております。完成品
商品編集画面(プレビュー画像部)
商品編集画面(カテゴリー部)
商品編集画面(販売手数料・利益部)
更新成功時の遷移画面
実装手順
1.ルーティング編集
- update_doneのルート設定(更新成功時の遷移画面)
2.コントローラー編集
- editメソッド設定
- updateメソッド設定
- エラーハンドリング
- 画像削除
- update_daneメソッド設定
3.ビュー編集・作成
- プレビュー画像呼び出し
- カテゴリー呼び出しの調整
- 更新成功時の遷移画面
4.JS編集
- プレビュー画像及びinputタグの生成、削除
- 販売手数料・利益の表示
1.ルーティング編集
update_doneルートを生成します。
これは、更新成功時の遷移画面を表示するためのルーティングです。config/routes.rbresources :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 end2.コントローラー編集
今回編集したコントローラーの記述は下記の通りです。
app/controller/items_controller.rbclass 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.rbclass 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.rbclass 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.rbdef update_done @item_update = Item.order("updated_at DESC").first end3.ビュー編集
全ての記述を載せると長くなってしまうのでここでは割愛して記述させて頂きます。
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("円"); } }); });これで編集機能完成です。
ここまで読んでくださり、ありがとうございます。
- 投稿日:2020-06-28T17:57:09+09:00
chat app.にてユーザー検索後、グループ追加を実装
はじめに・目的
- chat app.にてユーザー検索後、グループ追加を実装
- ユーザー検索をインクリメンタルサーチ
- サーチ後、追加ボタンでグループ追加。
- 削除ボタンでメンバー削除
- gif https://i.gyazo.com/00f3a7ce88036db231b951564a69c8a2.mp4
インクリメンタルサーチ実装
1.API側準備
- routing
routes.rbresources :users, only: [:index]
- users_controller.rbにindex定義
users_controllers.rbclass UsersController < ApplicationController def index respond_to do |format| format.html format.json end end
- app/views/usersディレクトリにindex.json.jbuilderファイルを作成
index.json.jbuilderjson.array! @users do |user| json.id user.id json.name user.name end2.テキストフィールド作成
_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-user3.テキストフィールドに入力するたび、イベント発火
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.rbdef 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 endparams[: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-id
とdata-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(); }); });
- 投稿日:2020-06-28T17:33:48+09:00
【Rails Webpacker】Webpacker::MissingEntryErrorのエラーはNode.jsのバージョンに起因するかもしれない話
【Rails Webpacker】Webpacker::MissingEntryErrorのエラーはNode.jsのバージョンに起因するかもしれない話
結論から
Nodeのバージョンを13.7以上にしてあげると
app>public>packsが生成され、当該エラーが解決される解決までのプロセス
普通に
rails new appname
でアプリを作成して
コントローラーやモデルの作成を行った後に、ブラウザでアプリをみてみると
このようなエラーが出る時がある。
多数のサイトを見て、以下のコマンドを打ってみてもうまくいかない。
terminalyarn 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 ~
をしたときに、以下のようなエラーが出た。terminalerror 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のバージョンを一覧で出力terminalnodebrew ls-remote
Node.jsの環境(ディレクトリ構成など)を自動的にセットアップしてくれる「setup」コマンドを実行しておきます
terminalnodebrew setup
インストールできるNode.jsのバージョンを一覧で出力
terminalnnodebrew install-binary v13.8.0
特定のバージョンの使用を命令
terminalnodebrew use v13.8.0
以上によってnodeのバージョンを変えてあげると、当該エラーが消え、うまく画面が表示されるようになった!!
ターミナルを見てみると、
terminalCompiled all packs in /Users/nakayama_kazuhito/Desktop/node-version/public/packs
どうやらnodeが結構public直下にある、webpack関連のファイルの生成に関与しているそう。
んーとりあえず、一旦解決!!
またおって原因分析をします!
それではまた!!☺️
- 投稿日:2020-06-28T17:31:16+09:00
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_tag
→javascript_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.jsximport 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 と連携していけているアプリケーションを作れるようになりたいなーと思っています。
- 投稿日:2020-06-28T17:23:00+09:00
Rails+vue+Parcel環境でSFCのスタイルが効かなかった件
単一コンポーネントで定義したスタイルが反映されない件
Rails+vue+Parcel環境で開発をやっていた時vueのSFCで書いたスタイルが適用されない
いろいろ調べても記事はあまりなかった。
コードをしっかり確認したら解決したのでまとめてみました。解決法
CSSを読み込むヘルパーメソッドを自作する
app/helpers/application_helper.rbmodule 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.rbmodule 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を読み込む!後書き
ドキュメントに沿ってコピペだけしてた部分でこうした問題があったので、しっかりコードをよもうと思いました。
- 投稿日:2020-06-28T16:00:34+09:00
deviseのルーティングをカスタマイズする
はじめに
deviseは自動的にもろもろの設定を追加してくれる便利なGemですが、ルーティングをスタマイズすることになって苦戦したので、調べたことをまとめます。
deviseはさまざまなコントローラがありますが、基本的なDevise::RegistrationsControlleコントローラーとそのルーティングに絞ってをみていきます。
カスタマイズしない場合
デフォルトのルーティングはこの様になっています。
config/routes.rbRails.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#createpathオプションでリソースのパスを変更する
pathオプションを追加して
auth
と指定します。
こうすると、パスのリソースに相当するcurstomers
がauth
へとが変わります。config/routes.rbRails.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#createpath_namesオプションでパスを追加する
path_namesオプションを追加して
registration
にregister
を指定ます。
こうすると、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#createskipオプションで対応付けをスキップする
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参考
- 投稿日:2020-06-28T15:57:36+09:00
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分やることで、前回から思い出す時間が省けました。
- 投稿日:2020-06-28T15:54:55+09:00
Rails:パンくずリストに2ページ目以降に「Xページ」と表示する方法
ページネーション機能がある一覧表示ページのパンくずリストに、「Xページ目」と表示するための実装方法を紹介します。
テキストだとちょっと伝わりづらいですが、
のように、「1ページ目と2ページ目以降でことなる表示」にし、「2ページ目以降にはページ数を表示する」、ということを今回の仕様とします。
環境
自作のプロテイン口コミサービスに導入( https://github.com/yuki0920/supplebox )のアイテム一覧ページ(/products/index)に導入すること前提に説明します。
また、ページネーション機能は
kaminari
で、パンくずリスト機能はgretel
ですでに実装されていることを想定しています。# Gemfile gem 'kaminari' gem 'gretel'現状
1ページ目でも2ページ目以降でもページ名(アイテム一覧)が表示されています。
gretelのオーソドックスな使い方をしています。
config/breadcrumbs.rbcrumb :products do link 'アイテム一覧', products_path parent :root endapp/views/products/index.html.haml- breadcrumb :productsXページ目を表示する
設定ファイルに追記します。parentにproductsを設定します。ページ数をpageとして第2引数で受け取り、
pageを使ってページ数を表示するようにします。第2引数で受け取った値を表示できる、というのはREADMEのサンプル集に載っています。
config/breadcrumbs.rbcrumb :products do link 'アイテム一覧', products_path parent :root end # 追記 crumb :products_pagination do |page| link "#{page}ページ目" parent :products endapp/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.rbdef 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
- 投稿日:2020-06-28T15:45:47+09:00
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 でつないでいます。
- 投稿日:2020-06-28T15:33:22+09:00
http://localhost:3000/rails/infoとは
にブラウザからアクセスするとルーティングの一覧が表示される。
- 投稿日:2020-06-28T14:42:14+09:00
【Redis】” SCAN ” による安全なKey取得を Rails で実装する
この記事では「
SCAN
によるKey取得をRailsで実装する例」を紹介します。Key取得の方法には
KEYS
やSCAN
がありますが、「サーバ稼働の安全性」を考慮するとKey取得にはSCAN
を使うことが推奨されています。詳しくは先日僕が投稿した「【Redis】KEYSによるKey取得の危険性とSCANによる安全な対処」をご覧ください。「
SCAN
でのKeyを取得」を実装して安全にキャッシュデータの削除や更新を行えるようにしましょう。実装例
app/models/concerns/cache_util.rbmodule 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.rbclass 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には多くのコマンドがあるみたいなので勉強しておこうと思います。
質問・ご指摘はコメント欄にお願いします。
- 投稿日:2020-06-28T14:25:58+09:00
Rspec 架空モデルのアソシエーション
投稿背景
架空モデルが関係する部分でテストを実装していたところ、架空モデルの定義でエラーが解決できずにいて手こずっていたが、なんとか解決できたので備忘録として投稿
モデルのアソシエーション
chat_notice.rbbelongs_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: truetourist.rbhas_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: :destroyguide.rbhas_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: :destroyfactory
chat_notice.rbFactoryBot.define do factory :chat_notice do association :chat association :guide_visitor association :guide_visited association :tourist_visitor association :tourist_visited end end実行したテスト
chat_notice.rbrequire '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
- 投稿日:2020-06-28T13:24:59+09:00
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(中級編)の基本用語をまとめました。
- 投稿日:2020-06-28T13:07:13+09:00
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しなくてはならない。
- 投稿日:2020-06-28T12:34:00+09:00
rails カラムの追加
初投稿です。
アウトプットがてら記事を投稿します。
記事を投稿する経緯は自分が忘れてしまうからです。
今回は新しくカラムを追加したり、削除する際の方法を記載いたします。
- カラムの追加
- カラムの削除
【1. カラムの追加】
テンプレートは下記です。rails g migration Addカラム名Toテーブル名 カラム名:型名実際にテーブルにカラムを追加する際の一例を記載いたします
ターミナルを開き下記のものを記述いたします。rails g migration AddNameToLists name:stringLists(テーブル)にstring型のNameカラムを追加することになります。
最後は下記で更新。rails db:migrateこれで\db\migrateに新しいマグレーションファイルが格納されているはずです。
【2. カラムの削除】
テンプレートは下記です。rails g migration Removeカラム名Fromテーブル名 カラム名:型名実際にカラムを削除する際の一例を記載いたします
ターミナルを開き下記のものを記述いたします。rails g migration RemoveNameFromLists name:stringLists(テーブル)からstring型のNameカラムを削除します。
最後は下記で更新。rails db:migrateこれで\db\migrateから消えてるはずです。
- 投稿日:2020-06-28T10:26:19+09:00
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に限った用語でないものも入っていますが、合わせて学習したのでこのまま記述しておきます。
- 投稿日:2020-06-28T10:13:59+09:00
日本語の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.ymlRailsのデフォルトのエラーメッセージや、日時などのフォーマットの日本語訳を手に入れる
Githubに有志による翻訳ファイル、rails-i18nのがありますので、それを感謝しながら利用します。
Gemfilegem 'rails-i18n'フォームの項目名や、エラーメッセージを日本語化
ちゃんと日本語化をしたい理由がこれだと思います。
項目名をあちこちに書いたりするのは大変ですよね…。
先に書いた設定をして、モデルや属性の訳語を登録すれば、こんなふうに表示されるようになります。モデルの訳語
config/locales/models/blog/ja.ymlja: activerecord: models: blog: 記事 attributes: blog: title: タイトル body: 本文 user_id: 著者 status: 状態 created_at: 作成日時 updated_at: 更新日時エラーメッセージの表示
モデルに対して
save
やvalid?
を行うと、errors.fullmessages
に項目名を含んだエラーメッセージが入っているので、それを表示しています。(ソース)app/views/shared/_form_error_messages.html.erb: <% model.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> : <% end %>モデルや属性の訳語の取得
フォーム
入力項目のラベルを
#label
で翻訳された属性名で作れます。(ソース)フォーム以外
確認画面などで入力フォームの項目名と合わせるのに、手書きでがんばらなくても
#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
で翻訳されたモデル名も取得できます。(…あんまり使いみちが思いつかず、コンソールですみません…。)
フォームオブジェクト
モデルじゃないけどモデルっぽいものや、複雑だったりするフォームを作るときにはActiveModelを使ったフォームオブジェクトを使いますが、同じようにできます。ただし、訳語ファイルの項目名が
active_record
ではなくactive_model
になっています。(訳語のソース、フォームのソース)config/locales/models/forms/session/ja.ymlja: activemodel: models: forms/session: セッション attributes: forms/session: login_id: ログインID password: パスワード日時表示などの日本語化
日時などを文字列にしようとするときに、つい今だけよければ…と思って、テンプレート内で
#strftime
を使ってしまったりします…。こういうことを繰り返して、アプリケーション内でびみょーーーーーうな差異が散らばってしまって、あとで悶々としてしまうことが何度もありました。
I18n.l
を使えば、もう少しまとまりを作ることができると思います。上の例で書いてあるフォーマットは、rails-i18nのこのあたりに定義があります。
最後の:nengetsu
はありませんが、これは訳語ファイルの中で定義したものです。
これらを使えば、多少バリエーションも固定化できると思います。(まだ成功体験はないです。)config/locales/views/defaults/ja.ymlja: date: formats: nengetsu: "%y年%m月"おわりに
Railsガイドをあらためて読み直すと、実感できずにスルーしていた部分に気づけてよいな、と思いました。
用意されている便利な機能を使って、なるべくがんばらないようにしたいです。
- 投稿日:2020-06-28T10:13:44+09:00
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を実行したら無事インストールできた。
- 投稿日:2020-06-28T02:13:06+09:00
rails フォーム 初期値
フォームのところに
<%= f.text_field :name%><%= f.text_field :name, value: "名無し" %>
value: で設定する。
- 投稿日:2020-06-28T01:46:47+09:00
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.rbclass 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.rbclass Post < ApplicationRecord enum state: { draft: 0, published: 1 } endapp/models/draft_post.rbclass DraftPost < Post endapp/models/published_post.rbclass PublishedPost < Post endSTI/enumのカラムがとりうる値のレコードを作成しておく
app/models/post/state.rbclass 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 endapp/models/post/type.rbclass 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も同じ手法で実装できるのでぜひやってみてください。