- 投稿日:2019-11-27T23:24:17+09:00
RailsアプリのViewをReactに変えた後、デプロイする手順
この記事は?
Railsサーバーと通信するReactアプリをデプロイする時の、自分が踏んだ手順を紹介します。
以前作成したRailsアプリのViewをReactで作り直し、最低限の機能実装まではできたのでデプロイしようとしたら手間取りました。手順
以下、自分が踏んだ手順を紹介します。
git pull
Jenkinsとか使ってる人はいらないでしょう。
npm install -g npm
そのまま
npm install
しようとしたら、先にやるように警告された。
そのまま打ったら「permission denied」エラーを出したのでsudo
をつけたnpm install
create react app
で使用するパッケージを読み込む
終わったらnode_modules
ディレクトリが増えていることを確認します。npm run build
Reactをこれでビルドします。
が、ここで問題が発生
$ npm run build > client@0.1.0 build /var/www/rails/foreign_books_search/client > react-scripts build Creating an optimized production build... Failed to compile. ./src/App.js Cannot find file './css/App.css' in './src'.確認してみると、そもそも
./src/css/
ディレクトリが存在しない。よく考えたら、自分はCSSではなくSCSSを使用しているので、Reactをビルドする前にまずSCSSをビルドしなければならない。
というわけで、自分が作成したgulpfile.js
'use strict'; var gulp = require('gulp'); var sass = require('gulp-sass'); sass.compiler = require('node-sass'); gulp.task('default', function () { return gulp.src('./src/**/*.scss') .pipe(sass().on('error', sass.logError)) .pipe(gulp.dest('./src/css')); }); gulp.task('sass:watch', function () { gulp.watch('./sass/**/*.scss', ['sass']); });なので、以下のコマンド
$ gulp -bash: gulp: コマンドが見つかりません...?
node_modulesにPATHが通ってない!
$ export PATH=$PATH:node_modules/.bin $ gulp -v CLI version: 2.2.0 Local version: 4.0.2 $ gulp [23:02:47] Using gulpfile /var/www/rails/my_app/client/gulpfile.js [23:02:47] Starting 'default'... [23:02:47] Finished 'default' after 53 ms
./src/css/
があることを確認したので、改めてReactのビルドを行います。$ npm run build > client@0.1.0 build /var/www/rails/foreign_books_search/client > react-scripts build Creating an optimized production build... Failed to compile. ./src/App.js Cannot find module: '@material-ui/core'. Make sure this package is installed. You can install this package by running: yarn add @material-ui/core.今度はmaterial-uiが無いと。。。
指示に従いインストール
$ yarn add @material-ui/core yarn add v1.15.2 ※色々表示される Done in 92.95s.3度めの正直を祈って!
$ npm run build > client@0.1.0 build /var/www/rails/foreign_books_search/client > react-scripts build Creating an optimized production build... Compiled successfully. File sizes after gzip: 92.81 KB build/static/js/2.f13f31dd.chunk.js 3.75 KB build/static/js/main.29ffb305.chunk.js 772 B build/static/js/runtime-main.b240f91f.js 681 B build/static/css/main.9ea7ac83.chunk.css 580 B build/static/css/2.94155858.chunk.css The project was built assuming it is hosted at the server root. You can control this with the homepage field in your package.json. For example, add this to build it for GitHub Pages: "homepage" : "http://myname.github.io/myapp", The build folder is ready to be deployed. You may serve it with a static server: yarn global add serve serve -s build Find out more about deployment here: https://bit.ly/CRA-deployCompiled successfully !
build/
ディレクトリにindex.html
があることも確認しました。Nginxの設定
設定ファイルの
root
をbuild/
ディレクトリにする設定の読み込み
$ sudo systemctl reload nginxその後、ドメインにアクセスし、アプリが表示されることを確認!
ついでに、今後新しい修正を反映するときは
npm run build
やunicornの再起動を忘れずに。
- 投稿日:2019-11-27T23:06:59+09:00
【Rails】メールアドレスの一意性の検証【Rails Tutorial 6章まとめ】
メールアドレスはユーザーごとに一意であり、重複してはならない。
既存のユーザーと同じメールアドレスを持つユーザーは無効となるように、テストを書く。test/models/user_test.rbtest "email addresses should be unique" do duplicate_user = @user.dup @user.save assert_not duplicate_user.valid? enddupメソッドは、@userインスタンスをコピーする。
@userが保存された後は、duplicate_userは無効でなければならない。emailのバリデーションにuniqunessオプションを追加して、重複できないようにする。
app/models/user.rbvalidates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: trueメールアドレスの大文字小文字
ところで、メールアドレスは大文字小文字を区別しないらしい。
つまり、foo@bar.comとFOO@BAR.COMとFoO@BAr.coMは同じである。そこで、重複するユーザーのメールアドレスは、既存ユーザーのメールアドレスを大文字にして入れる。
test/models/user_test.rbtest "email addresses should be unique" do duplicate_user = @user.dup duplicate_user.email = @user.email.upcase @user.save assert_not duplicate_user.valid? endメールアドレスの文字列を大文字にするために、upcaseメソッドを使っている。
現在のバリデーションでは、メールアドレスの大文字小文字を区別しているので、テストは失敗する。
@user.emailとduplicate_user.email(= @user.email.upcase)は別物と認識されるので、duplicate_userが有効となるからである。メールアドレスの大文字小文字を区別しないようにするためには、uniquenessオプションにcase_sensitive: falseを設定する。
app/models/user.rbvalidates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }テストをして、成功することを確認する。
データベースレベルでの一意性の検証とインデックス
ユーザー登録ボタンを2回クリックしたりすると、データベースに同じユーザーが二つ作成されてしまうことがあるらしい。
たまに「注文ボタンは一回だけ押してください」とか、「ボタンをクリックした後、ページの読み込みが遅くてもそのままお待ちください」とか書いてあることがあるが、それのことである。これを解決するために、Userモデルのemailカラムにインデックスを付け、その一意性を設定する。
インデックスは本来、データベースの検索を高速化するためのものである。Userモデルを変更するため、マイグレーションファイルを作成し、インデックスを追加する。
$ rails generate migration add_index_to_users_emaildb/migrate/[timestamp]_add_index_to_users_email.rbclass AddIndexToUsersEmail < ActiveRecord::Migration[5.0] def change add_index :users, :email, unique: true end enduniqueオプションをtrueとすることで、インデックスの一意性が保たれる。
ところで、バリデーションでは"uniqueness"と名詞だったのに、インデックスでは"unique"と形容詞なのはなぜ?マイグレーションを実行する。
rails db:migrateここで、テスト用のユーザーデータを入れておくfixtureファイルに、同じメールアドレスが設定されたユーザーが存在しているために、テストが失敗する。
test/fixtures/users.ymlone: name: MyString email: MyString two: name: MyString email: MyStringこのfixtureファイルは、Userモデルを生成した際にできたものである。
これを空にしておき、テストが成功することを確認する。test/fixtures/users.yml# 空にする (既存のコードは削除する)
メモ:なぜかここでエラーが出た。
ActiveRecord::PendingMigrationError ( Migrations are pending. To resolve this issue, run: bin/rake db:migrate RAILS_ENV=development ):インデックス用のマイグレーションファイルを削除して、db:migrateを実行。
マイグレーションファイルを作り直してdb:migrateを再び実行すると直った。もう一つの問題
さらにもう一つ問題があるらしい。
「いくつかのデータベースのアダプタが、常に大文字小文字を区別するインデックス を使っているとは限らない問題への対処です。例えば、Foo@ExAMPle.Comとfoo@example.comが別々の文字列だと解釈してしまうデータベースがあります」
とのこと。データベースのアダプタとやらがイマイチよく分からないが、とにかくこれを解決するため、データベースにメールアドレスを保存する際には小文字に変換して保存するようにする。
before_saveメソッドを使う。app/models/user.rbclass User < ApplicationRecord before_save { self.email = self.email.downcase } validates :name, presence: true, length: { maximum: 50 } . . . endメールアドレスを小文字化して代入し直す。
selfは保存されるUserインスタンスを指す。self.email = self.email.downcaseは、右側のselfを省略して
self.email = email.downcaseとも書ける。
!を使って次のように書くと、email属性を直接変更できるようである。
email.downcase!小文字化に対するテスト
メールアドレスが保存される際に、小文字化されているかをテストする。
test/models/user_test.rbtest "email addresses should be saved as lower-case" do mixed_case_email = "Foo@ExAMPle.CoM" @user.email = mixed_case_email @user.save assert_equal mixed_case_email.downcase, @user.reload.email end注:最後のassert_equalで、なぜかreloadメソッドを使用している。
@userは保存された後なので、reloadをしても特に意味はないはずである。
実際にコンソールで確認したが、@user.emailと@user.reload.emailは同じ値を返し、reloadを除いてもテストに問題はなかった。
- 投稿日:2019-11-27T21:46:02+09:00
【Rails】Userモデルの基本的なバリデーションとテスト【Rails Tutorial 6章まとめ】
最初のモデルが有効であるかのテスト
生成したモデルの属性に値を与え、インスタンスが有効であるかをテストする(バリデーションはまだ無い)。
test/models/user_test.rbrequire 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end test "should be valid" do assert @user.valid? end endassertは引数の論理値がtrueであることを確認する。
assert_notはその逆で、falseであることを確認する。存在性の検証(presence)
name属性とemail属性は必ず存在していなければならない。
そこで、バリデーションを設定して各属性が空白の場合は有効でないようにする。テスト駆動開発で進めていくので、バリデーションを設定する前に、バリデーションが無ければ失敗するテストを書く。
test/models/user_test.rbtest "name should be present" do @user.name = " " assert_not @user.valid? end test "email should be present" do @user.email = " " assert_not @user.valid? endnameとemailにそれぞれ空白を入れる。
バリデーションが無いので@user.valid?はtrueを返し、assert_notに反するため、テストは失敗する。テストを成功させるために、Userモデルにバリデーションを設定する。
app/models/user.rbclass User < ApplicationRecord validates :name, presence: true validates :email, presence: true endテストをして、成功することを確認しておく。
長さの制限
nameとemailは長すぎないほうが良い。
長すぎるnameとemailを制限するバリデーションを、テストを書いてから設定する。test/models/user_test.rbtest "name should not be too long" do @user.name = "a" * 51 assert_not @user.valid? end test "email should not be too long" do @user.email = "a" * 244 + "@example.com" assert_not @user.valid? endnameは50文字まで、emailはデータベースの文字列の限界である255文字までとする。
そこでnameに51文字、emailに256文字を入れておく。app/models/user.rbclass User < ApplicationRecord validates :name, presence: true, length: { maximum: 50 } validates :email, presence: true, length: { maximum: 255 } endpresenceオプションとlengthオプションはそれぞれをキーとする一つのハッシュだが、validatesメソッドの最後の引数なので、{}は無くてもよい。
lengthキーの値はハッシュなので、{}が必要である。テストをして、成功することを確認しておく。
emailのフォーマットの検証
現在のままではemailにメールアドレス以外を入れても保存できてしまうので、フォーマットを制限する。
バリデーションの前に、有効なメールアドレスを確認するテストと、無効なメールアドレスを確認するテストを書く。
test/models/user_test.rbtest "email validation should accept valid addresses" do valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn] valid_addresses.each do |valid_address| @user.email = valid_address assert @user.valid?, "#{valid_address.inspect} should be valid" end end test "email validation should reject invalid addresses" do invalid_addresses = %w[user@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com] invalid_addresses.each do |invalid_address| @user.email = invalid_address assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" end end変数にいくつかの有効なメールアドレスを配列にして代入し、eachメソッドを使ってそれぞれのメールアドレスを@user.emailに代入、assertで有効であることを確認する。
無効なメールアドレスのほうのテストはその逆である。
まだバリデーションが無いので、上のテストは成功し、下のテストは失敗する(無効なアドレスも有効と見なされるから)。assertの第二引数に文字列を与えると、テストが失敗した際にエラーメッセージとして表示することができる。
inspectメソッドは「オブジェクトを分かりやすい文字列にして返す」メソッドらしいが、イマイチよくわからない。なお、%w[]を使うと、文字列の配列を簡単に作ることができる。
>> %w[foo bar baz] => ["foo", "bar", "baz"]フォーマットの設定
フォーマットを設定するためには、formatオプションを使う。
validates :email, format: { with: /<regular expression>/ }/<regular expression>/のところに正しいメールアドレスを表す正規表現(Regurar Expression, regex)を入れる。
正規表現を理解する必要は今のところ無いので、コピペして以下のようにする。app/models/user.rbclass User < ApplicationRecord validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX } endテストをして、成功することを確認しておく。
- 投稿日:2019-11-27T19:39:10+09:00
link_to で FontAwesomeのフォントをリンクにする
やりたいこと
スマホアプリでアイコンをタップすると別画面に遷移するのと同様に、Webページ上でアイコンをリンク化し、クリックすると別ページへ飛ぶようにしたい。
環境
Ruby 2.6.5
Ruby on Rails 5.1.7
Bootstrap 4
FontAwasome 4.7.0書き方
<%= link_to("", "login", {class: "fa fa-user"}) %>結果
要するに…
FontAwasomeのフォントはhtmlタグのclass属性にセレクタを指定すれば表示されるようになるので、
link_to の場合でもclassに指定してあげればFontAwasomeのフォントがリンク化するという話でした。
(最初はこれが分からなかった…)class を囲っている中カッコは不要かもしれません。
- 投稿日:2019-11-27T18:53:22+09:00
Railsチュートリアル 第10章 - 「div.paginationが無い」と言われてテストが通らない場合
何が起こったか
Railsチュートリアルも第10章まで進み、ユーザー一覧のテストを一通り書き終えました。しかし、以下のように「
div.pagination
が無い」と言われてテストが通りません。# rails test test/integration/users_index_test.rb Running via Spring preloader in process 12045 Started with run options --seed 11902 FAIL["test_index_including_pagination", UsersIndexTest, 2.137264299992239] test_index_including_pagination#UsersIndexTest (2.14s) Expected at least 1 element matching "div.pagination", found 0.. Expected 0 to be >= 1. test/integration/users_index_test.rb:12:in `block in <class:UsersIndexTest>' 1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.14716s 1 tests, 2 assertions, 1 failures, 0 errors, 0 skips解決
app/views/users/index.html.erb
やapp/controllers/users_controller.rb
の記述に間違いがないのにテストが通らない場合、fixtureの記述を間違えているのが原因かもしれません。私の場合は、fixtureにおける以下の記述間違いが原因でした。
test/fixtures/users.yml(抜粋)... <% 30.times do |n| %> - user_<% n %>: + user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %>これは気が付きにくい。
- 投稿日:2019-11-27T18:53:22+09:00
Railsチュートリアル 第10章 - 「div.paginationが無い」と言われてテストが失敗する場合
何が起こったか
Railsチュートリアルも第10章まで進み、ユーザー一覧のテストを一通り書き終えました。しかし、以下のように「
div.pagination
が無い」と言われてテストが通りません。# rails test test/integration/users_index_test.rb Running via Spring preloader in process 12045 Started with run options --seed 11902 FAIL["test_index_including_pagination", UsersIndexTest, 2.137264299992239] test_index_including_pagination#UsersIndexTest (2.14s) Expected at least 1 element matching "div.pagination", found 0.. Expected 0 to be >= 1. test/integration/users_index_test.rb:12:in `block in <class:UsersIndexTest>' 1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.14716s 1 tests, 2 assertions, 1 failures, 0 errors, 0 skips解決
app/views/users/index.html.erb
やapp/controllers/users_controller.rb
の記述に間違いがないのにテストが通らない場合、fixtureの記述を間違えているのが原因かもしれません。私の場合は、fixtureにおける以下の記述間違いが原因でした。
test/fixtures/users.yml(抜粋)... <% 30.times do |n| %> - user_<% n %>: + user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %>これは気が付きにくい。
- 投稿日:2019-11-27T18:26:47+09:00
【Rails】ユーザーと投稿を関連づける
※この記事はProgate Railsコースをやってみて開発する際使えそうなおおまかな手順をメモしただけです。
投稿にユーザの名前と画像を表示
- 投稿を管理しているテーブルに「どのユーザが投稿したのか」という情報を管理するためのカラム(user_id)を追加
- 「新規投稿の際にどのユーザーが投稿したのか」という情報を(user_id)に追加
- あとは表示するときに使うアクションでuser_idを使ってユーザ情報を集めてビューで表示
ユーザ詳細にそのユーザの投稿一覧を作る
- ユーザ詳細アクションでそのユーザと一致するuser_idの投稿をwhereメソッドで集めてビューで表示
投稿の編集と削除の制限
- 投稿詳細表示のアクションででログインしているユーザのidと投稿のuser_idが一致したら「編集」と「削除」のリンクを表示
- 投稿編集表示メソッド・投稿編集メソッド・投稿削除メソッドなどで現在ログインしているユーザのidと投稿のユーザidが一致しなければ投稿一覧へリダイレクト
「いいね」機能の追加
- Likeモデルと「いいねを押したユーザのID」と「いいねを押した投稿」を管理するためのlikesテーブルを作成
- likesテーブルを用いて操作するためのlikesコントローラを作成してdestroyメソッドとcreateメソッドを作成する
- いいねボタンの表示とcreateアクションとdestroyアクションの結びつけ
- いいねの数を表示
ヒント
・いいねボタンの表示
link_toメソッドにHTML要素を含む場合<% link_to("URL") do %> HTML要素 <% end %>・いいねの数の表示
whereメソッドとcountメソッドを使用いいねした投稿の表示
- likes.html.erbとそのルートとコントローラを作成する
- likesテーブルをもとにそのユーザがいいねした投稿を表示する
パスワードの暗号化
bcrypt
というgemをインストール- usersテーブルに
password_digest
カラムを追加してpassword
カラムを削除- 保存する際にパスワードを暗号化するために
has_secure_password
をuserモデルに追加ヒント
・passwordカラムを削除
remove_columnメソッドを使用暗号化されたパスワードでログインする
- authenticateメソッドを使用して、入力されたパスワードと比較して、一致すればログインできるようにする
ヒント
authenticateメソッドは渡された引数を暗号化する
- 投稿日:2019-11-27T18:22:55+09:00
【Rails tutorial】7章まとめ
app/application.hrml.erb<!DOCTYPE html> <html> . . . <body> <%= render 'layouts/header' %> <div class="container"> <%= yield %> <%= render 'layouts/footer' %> <%= debug(params) if Rails.env.development? %> </div> </body> </html>デバッグ情報は開発環境のみに表示されるようになる
controller: static_pages action: homeparamsに含まれてる内容でYAMLという形式で書かれている
RESTの原則に従う場合、リソースへの参照はリソース名とユニークなIDを使うのが普通
例)
ユーザーをリソース
id=1のユーザーを参照する
⇢ /users/1というURLに対してGETリクエストを発行するここでいうshowアクションは暗黙のリクエストになる
app/views/users/show.html.erb<%= @user.name %> , <%= @user.email %>app/controllers/user_controller.rbdef show @user = User.find(params[:id]) endユーザーのid読み出しにはparamsを使用
Usersコントローラにリクエストが正常に送信されると
params[:id]の部分はユーザーidの1に置き換わる
params[:id]は文字列型の'1'だが
findメソッドでは自動的に整数型に変換される7.2 ユーザー登録フォーム
<%= form_for(@user) do |f| %> . . . <% end %>doキーワードは、form_forが1つの変数を持つブロックを取ることを表す
fはformのf<%= f.label :name %> <%= f.text_field :name %>Userモデルのname属性を設定する、ラベル付きテキストフィールド要素を作成するのに必要なHTMLを作成
"user" => { "name" => "Foo Bar", "email" => "foo@invalid", "password" => "[FILTERED]", "password_confirmation" => "[FILTERED]" }上のハッシュはUsersコントローラにparamsとして渡される
このparamsハッシュには各リクエストの情報が含まれてる。
ユーザー登録情報の送信の場合、paramsには複数のハッシュに対するハッシュ(入れ子になったハッシュ)が含まれる。
上のデバッグ情報では、フォーム送信の結果が、送信された値に対応する
属性とともにuserハッシュに保存される7.3.2 Strong Parameters
以前のバージョンのRails
モデル層で
attr_accessible
メソッドを使ってたRails 4.0
コントローラ層で
Strong Parameters
を使うことが推奨されているStrong Parameters を使って
必須のパラメータと許可されたパラメータを指定することができる。
paramsハッシュをまるごと渡すとエラーが発生するので、Railsはデフォルトでマスアサインメントの脆弱性から守られるようになった。paramsハッシュでは
:user
属性を必須として、名前、メールアドレス、パスワード、パスワードの確認の属性をそれぞれ許可してそれ以外を許可しないようにしたいと考えてるparams.require(:user).permit(:name, :email, :password, :password_confirmation)このパラメータを使いやすくするために、user_paramsという外部メソッドを使うのが
慣習になっている@user = User.new(user_params)この
user_params
メソッドはUsersコントローラの内部でのみ実行され、Web経由で外部ユーザーにさらされる必要はないため、privateキーワードを使って外部から使えないようにする7.3.3 エラーメッセージ
errors.full_messages
オブジェクトはエラーメッセージの配列を持っている
shared/error_messages
Rails全般の慣習として、複数のビューで使われるパーシャルは専用のディレクトリshared
によく置かれる7.3.4 失敗時のテスト
新規ユーザー登録用の統合テストを生成
$ rails g integration_test users_signup invoke test_unit create test/integration/users_signup_test.rbユーザー登録ボタンを押した時に(ユーザー情報が無効だから)ユーザーが作成されないことを確認
getメソッドでユーザー登録ページアクセス
get signup_pathフォーム送信をテストするには users_pathに対してPOSTリクエストを送信する必要がある。これはpostメソッドを使って実現する
assert_no_difference 'User.count' do post users_path, params: {user: {name: "", email: "user@invalid", password: "foo", password_confirmation: "bar"}} endassert_no_difference メソッドのブロック内でpostを使い、メソッドの引数には'User.count'を与える。これはassert_no_differenceのブロックを実行する前後で引数の値(User.count)が変わらないことをテストしている。
⇢ ユーザー数を覚えた後にデータを投稿してみて、ユーザーが変わらないかどうかを検証するテストまとめ
debugメソッドを使うことで、役立つデバッグ情報を表示できる
Sassのmixin機能を使うと、CSSのルールをまとめたり他の場所で再利用できるようになる
Railsには標準で3つ環境が備わっており、それぞれ開発環境 (development)、テスト環境 (test)、本番環境 (production)と呼ぶ
標準的なRESTfulなURLを通して、ユーザー情報をリソースとして扱えるようになった
Gravatarを使うと、ユーザーのプロフィール画像を簡単に表示できるようになる
form_forヘルパーは、Active Recordのオブジェクトに対応したフォームを生成する
ユーザー登録に失敗した場合はnewビューを再描画するようにした。その際、Active Recordが自動的に検知したエラーメッセージを表示できるようにした
flash変数を使うと、一時的なメッセージを表示できるようになる
ユーザー登録に成功すると、データベース上にユーザーが追加、プロフィールページにリダイレクト、ウェルカムメッセージの表示といった順で処理が進む
統合テストを使うことで送信フォームの振る舞いを検証したり、バグの発生を検知したりできる
セキュアな通信と高いパフォーマンスを確保するために、本番環境ではSSLとPumaを導入した
- 投稿日:2019-11-27T17:30:55+09:00
特定パス以下のActionController::RoutingErrorでJSONを返却する
環境
Ruby 2.5.7
Rails 5.2 (ActionPack 5.2)コード
lib/middleware/routing_error_response_json.rbmodule YourNameSpace::Middleware class RoutingErrorResponseJson RESPONSE_JSON_PATHS = %w[/api/v1] def initialize(app) @app = app end def call(env) request = ActionDispatch::Request.new(env) status, headers, response = @app.call(env) # NOTE: ActionController::RoutingError時に設定されるパラメータ # See https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/action_dispatch/journey/router.rb#L64 if enable?(request) && status == 404 && headers['X-Cascade'] == 'pass' status, headers, response = pass_response(status) end [status, headers, response] rescue Exception => exception # rubocop:disable Lint/RescueException raise exception end private def pass_response(status) [ status, { "Content-Type" => "application/json" }, [ JSON.generate( { title: 'Routing Error', detail: 'No route matches', invalid_params: [] } ) ] ] end def enable?(request) RESPONSE_JSON_PATHS.any? { |path| request.path.to_s.start_with?(path) } end end endconfig/initializers/middleware.rbrequire_relative "../../lib/middleware/routing_error_response_json" ::Rails.application.config.middleware.use YourNameSpace::Middleware::RoutingErrorResponseJson実現できる事
↑の例であれば
/api/v1
以下へのリクエストでRoutingErrorとなった場合にJSONを返却する事ができる。
WebとApiが同居しているアプリケーションなどで利用価値があるかもしれない。最後に
もっと良い方法があれば教えて欲しいです。
- 投稿日:2019-11-27T17:24:32+09:00
クイズ:なぜバリデーションエラーになるでしょう
要約
- railsのcallbackで中間テーブルを生成する方法をミスるとvalidationエラーになるアンチパターン
詳細
- モデル構成
class User < ApplicationRecord has_many :notification_settings, dependent: :destroy has_many :notifications, through: :notification_settings, dependent: :destroy after_create :create_default_notifications end class NotificationSetting < ApplicationRecord belongs_to :user belongs_to :notification validates :user_id, uniqueness: { scope: [:notification_id] } end class Notification < ApplicationRecord has_many :notification_settings, dependent: :destroy has_many :users, through: :notification_settings, dependent: :destroy endとかで、ユーザに通知設定が複数ひもづいている状態で、ユーザが作成されたタイミングでデフォルトの通知設定を作りたくて
create_default_notifications
の中でdef create_default_notifications self.notifications = Notification.all endてやっていると一見OKっぽいんだけど、
validates :user_id, uniqueness: { scope: [:notification_id] }
で引っかかってvalildationエラーになる。理由
self.notifications = Notification.allが評価されたタイミングでNotificationSettingにinsertが発行されてしまって、
その後autosaveも動くので、結果2回createされてしまってuniqueじゃなくなってエラーになる。
has_manyにassignすると即insertされるというのがわかりにくかったという問題。
=はsaveされないという思い込みでした。修正
丁寧に1件1件buildする
Notification.all.each do |notification| self.notification_settings.build(notification: notification) end
- 投稿日:2019-11-27T16:42:16+09:00
【Rails】Railsでタグ付け機能を実装した時のメモ。
はじめに
掲示板やブログ等のアプリの投稿フォームで、タグのチェックボックスを選択し、表示する機能を作成します。
今回はgemのacts-as-taggable-on
は使用せずに実装してみたいと思います。バージョン
- ruby 2.6.3
- rails 6.0.1
- mysql 8.0.18
前提
- Rails環境構築済みであること
モデルの作成
まず、今回作成するするモデルは以下の3つになります。
Articles Article_tag_relations Tags id id id title article_id name body tag_id
- Articles
- 掲示板の記事に関するテーブル
- Tags
- タグに関するテーブル
- Article_tag_relations
- 掲示板とタグを紐づけるための中間テーブル
では、さっそくモデルを作成していきます。
#Articleモデルのマイグレーションファイル生成 $rails g model Article title:string body:string #Tagモデルにマイグレーションファイル生成 $rails g model Tag name:string #Article_tag_relationモデルのマイグレーションファイル生成 $rails g model Article_tag_relation article:references tag:references今回はTagsテーブルのnameカラムにはnullを許容しないので、マイグレーションファイルを修正します。
xxxxxxxxxxxx_create_tags.rbclass CreateTags < ActiveRecord::Migration[6.0] def change create_table :tags do |t| t.string :name, null: false t.timestamps end end endそれでは、DBに反映させます。
$rails db:migrateモデルのアソシエーションの設定
article_tag_relation.rbclass ArticleTagRelation < ApplicationRecord belongs_to :article belongs_to :tag endarticle.rbclass Article < ApplicationRecord #Articlesテーブルから中間テーブルに対する関連付け has_many :article_tag_relations, dependent: :destroy #Articlesテーブルから中間テーブルを介してTagsテーブルへの関連付け has_many :tags, through: :article_tag_relations, dependent: :destroy endtag.rbclass Tag < ApplicationRecord #Tagsテーブルから中間テーブルに対する関連付け has_many :article_tag_relations, dependent: :destroy #Tagsテーブルから中間テーブルを介してArticleテーブルへの関連付け has_many :articles, through: :article_tag_relations, dependent: :destroy endタグのチェックボックスの作成
new.html.erb<%= form_for @article do |f| %> <div class='form-group'> <%= f.collection_check_boxes(:tag_ids, Tag.all, :id, :name) do |tag| %> <div class='form-check'> <%= tag.label class: 'form-check-label' do %> <%= tag.check_box class: 'form-check-input' %> <%= tag.text %> <% end %> </div> <% end %> </div> <%= f.submit '保存', class: 'btn btn-primary' %> <% end %>入力フォームにタグの選択チェックボックスを表示するのに、
collection_check_boxes
を使用します。
第一引数のtag_ids
はタグIDのリストを渡し、複数のタグを掲示板に紐づけることができます。
第二引数には、タグオブジェクトのリスト。
第三引数にチェックボックスのvalue、第四引数にタグオブジェクトのnameプロパティをラベル名に指定。
- この状態では、まだTagsの中身は空なので、試しにテストデータを投入してみます。
seeds.rbTag.create([ { name: 'タグ1' }, { name: 'タグ2' }, { name: 'タグ3' }, { name: 'タグ4' }, { name: 'タグ5' } ])$rails db:seed先ほど投入したタグデータのチェックボックスが表示されていますね。
コントローラーの作成
articles_controller.rbclass ArticlesController < ApplicationController def new @article = Article.new end def create article = Article.new(article_params) if article.save redirect_to article else redirect_to :back end end def show @article = Article.find(params[:id]) end def destroy @article.destroy redirect_to articles_path end private def article_params params.require(:article).permit(:title, :body, tag_ids: []) end end今回はあくまでタグを表示するだけなので、かなり簡易的に実装します。
def article_params params.require(:article).permit(:title, :body, tag_ids: []) endここで先ほどタグのチェックボックスで設定したtag_idsを許可します。
複数のtag_idが渡ってくるので配列の形式で記述しています。タグを表示する。
最後に、チェックしたタグを
show.html.erb
で表示してみたいと思います。show.html.erb<% @article.tags.each do |tag| %> <span class= 'badge badge-primary'><%= tag.name %></span> <% end %>このように複数のタグをチェックした場合でも、表示できれば完成です。
最後に
今回はタグをチェックボックスで選択し、記事に表示するまでの機能を実装してみました。
ユーザーが自由にタグを追加できたり、タグで絞り込みなどを行えるようにするなど個人アプリを作る場合には、更に幅を広げてみると面白いかもしれません。以上。
参考
- 投稿日:2019-11-27T16:35:49+09:00
これから作るRailsアプリの大まかな作成手順
0.Railsチュートリアルに準拠
1.rails 5.1.6 new 『名前』
2.gemの準備をしてから、gitしておく
bundle
git init
git add -A
git commit -m "Initialize repository"3.rails generate controller Home top #ホームコントローラとトップページを作る
root 'home#top'4.rails generate controller Post new #ポストコントローラとnewページを作る
get '/signup', to: 'users#new' #登録フォームページになる5.rails generate model User name:string #モデルを作る
rails db:migrate
resources :users
- 投稿日:2019-11-27T16:35:49+09:00
これから作るRailsアプリの大まかな作成手順~簡易画像投稿サイト~
0.Railsチュートリアルに準拠
1.rails 5.1.6 new 『名前』
2.gemの準備をしてから、gitしておく
bundle
git init
git add -A
git commit -m "Initialize repository"3.rails generate controller Home top #ホームコントローラとトップページを作る
root 'home#top'4.rails generate controller Post new #ポストコントローラとnewページを作る
get '/signup', to: 'users#new' #登録フォームページになる5.rails generate model User name:string #モデルを作る
rails db:migrate
resources :users6.RESTに従った設計にする
GET /users index users_path すべてのユーザーを一覧するページ
GET /users/1 show user_path(user) 特定のユーザーを表示するページ
GET /users/new new new_user_path ユーザーを新規作成するページ (ユーザー登録)
POST /users create users_path ユーザーを作成するアクション
GET /users/1/edit edit edit_user_path(user) id=1のユーザーを編集するページ
PATCH /users/1 update user_path(user) ユーザーを更新するアクション
DELETE /users/1 destroy user_path(user) ユーザーを削除するアクション7.topに登録、一覧表示へのリンクを表示。余裕があればtopを一覧表示にする。
8.クリックで特定画像ページへのリンク
9.一覧に編集と削除の表示。
10.form_forがきまれば、carrierとimagemagicもきまる。
11.一覧ページではclassで大きさを指定してきれいに並べ、実際のページでは原寸表示
.gazouookisa img {
width: 200px;
height: 200px;
}12.heroku設定。アップして動いたら完成
- 投稿日:2019-11-27T15:45:30+09:00
rails db:migrateに失敗してStandardError: An error has occurred, all later migrations canceledが出た場合の対処方法
バージョン
Rails 5.0.1rails db:migrateコマンドを実行した際に、
タイトルのエラーが出た時の対処方法メモです。エラーはこちら
# rails db:migrate == 20191126024458 CreateDataProvisionUserStatuses: migrating ================== -- create_table(:user_statuses) rails aborted! StandardError: An error has occurred, all later migrations canceled: Mysql2::Error: Table 'user_statuses' already exists: ~~~~対処方法として
テーブルが既に存在していると怒られています。
なので# rails db:reset
一度リセットしてから再び、
# rails db:migrate
これで解決しました。
参考
Rails:migrateでDBをリセットして最初からつくり直す方法。
https://qiita.com/Atsushi_/items/a230fb7f624d1eebf2f3
- 投稿日:2019-11-27T14:56:02+09:00
Mac環境でRails内で別プロセスが立ち上がる処理をすると落ちてしまうとき
概要
Railsで
task runner
を実行しようとすると以下のエラーが起こり、途中で処理が止まってしまいました。$ bundle exec rails runner -e development [タスク] objc[13178]: +[__NSPlaceholderDictionary initialize] may have been in progress in another thread when fork() was called. objc[13178]: +[__NSPlaceholderDictionary initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.解決法
$ echo 'export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES' >> ~/.bash_profile $ exec $SHELL -l $ bin/spring stop $ bundle exec rails runner -e development [タスク]参照
macOS High Sierra で "__NSPlaceholderDictionary initialize" エラー
- 投稿日:2019-11-27T14:49:12+09:00
rails_クリックでカウントアップ&データの保存
タイトルのように、クリックするごとに数字をカウントアップし、非同期でデータを保存する方法を紹介します
イメージとしては、動画を確認してください⬇️
— Kamotetu (@Kamotetu2) November 27, 2019自分もプログラミング初学者なので、もっとやり方あると思いますが、ググっても良い記事がなく、同じような実装に悩まれている方が多かったので、「とりあえず実装できる」レベルですが、誰かの参考になればと思います
あと、ここで例として紹介しているアプリケーションの内容は小説を定義(maintitle)し、その小説の内容(story)を随時アップしていくという内容になっていますので、なんとなくイメージしていただくと記事の内容が理解しやすいかなと思います
また、マイグレーションファイルの作成や、コントローラの作成など、ここで紹介していない部分の実装等については、別の方の記事等を参考にしてください
それではちょっと長くなりますが紹介していきます
- ruby 2.5.1
- rails 5.2.3
使用しているgem
- 'gon'
- 'device'
- 'jquery-rails'
○○_create_reviews.rbclass CreateReviews < ActiveRecord::Migration[5.2] def change create_table :reviews do |t| t.integer :user_id t.integer :story_id t.integer :maintitle_id t.integer :review t.timestamps end end endapp/models/review.rbclass Review < ApplicationRecord belongs_to :user belongs_to :story belongs_to :maintitle endconfig/routes.rbresources :maintitles do # 省略 resources :stories do post 'like_review' delete 'unlike_review' #まだ実装できてません・・ resources :comments, only: [:create] end endapp/controllers/stories.controller.rbbefore_action :set_maintitle, only: [:new, :create, :show, :like_review, # 省略] before_action :set_story, only: [:show, # 省略] //////////////////////////////////////////////////////// def show #set_maintitle #set_story @user = User.find(@story.user_id) if user_signed_in? @review = Review.find_by(story_id: @story.id, user_id: current_user.id) if @review != nil gon.my_review_count = @review.review + 1 else @review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0) gon.my_review_count = @review.review + 1 end end @reviews = Review.where(story_id: @story.id) @a = [] @reviews.each do |review| p = review.review @a.push(p) end @review_all_count = @a.sum gon.review_all_count = @a.sum + 1 end //////////////////////////////////////////////////////// def like_review # set_maintitle @story = Story.find(params[:story_id]) @review = Review.find_by(story_id: @story.id, user_id: current_user.id) p = @review.review + 1 @review.update(review: p) end //////////////////////////////////////////////////////// private def set_maintitle @maintitle = Maintitle.find(params[:maintitle_id]) end def set_story @story = Story.find(params[:id]) endapp/views/stories/show.html.erb# 省略 <%if @story.user_id != current_user.id%> <div class="like_review_area"> <%=link_to maintitle_story_like_review_path(@maintitle, @story), method: :post, remote: :true, class: :like_review_btn do%> <div class="like_review_link"> <div class="like_review", id="<%=@maintitle.id%>"> 面白かった数だけここをクリック! </div> </div> <%end%> </div> <%end%> <%if user_signed_in?%> <%if @story.user_id != current_user.id%> <div class="show_review_my_count_area"> あなたの評価: <i class="fa fa-thumbs-o-up "> </i> <i class="review_my_count_area"> <%if @review.present?%> <i class="my_count"> <%=@review.review%> </i> <%else%> <i class="my_count"> 0 </i> <%end%> </i> </div> <%end%> <%end%>app/assets/javascripts/like_review.jsfunction appendFaThoumbsOUpMore(my_review_count) { var fa_thumbs_o_up_more = $(".review_my_count_area"); var my_count_more = `<i class="my_count"> ${my_review_count} </i>` fa_thumbs_o_up_more.append(my_count_more); }; function appendFaThoumbsOUpAll(all_review_count) { var fa_thumbs_o_up_all = $(".review_all_count_area"); var all_count = `<i class="all_count"> ${all_review_count} </i>` fa_thumbs_o_up_all.append(all_count); }; $(function() { $(document).on("ajax:success", ".like_review_btn", function(e) { e.preventDefault(); $(".my_count").remove(); $(".all_count").remove(); var my_review_count = gon.my_review_count++ ; var all_review_count = gon.review_all_count++ ; appendFaThoumbsOUpMore(my_review_count); appendFaThoumbsOUpAll(all_review_count); }); });ずらずら記載してすいません
ということでポイントを説明していきます
reviewテーブルを作成
自分は、誰がどの投稿に対して評価をつけたかわかるように、
○○_create_reviews.rbclass CreateReviews < ActiveRecord::Migration[5.2] def change create_table :reviews do |t| t.integer :user_id t.integer :story_id t.integer :maintitle_id t.integer :review t.timestamps end end endとしましたが、単に評価の数だけならuser_idとかいらないと思います
あとそれぞれにnull: false foreign_key: trueもつけた方がいいかもですね
modelにアソシエーションを定義
app/models/review.rbclass Review < ApplicationRecord belongs_to :user belongs_to :story belongs_to :maintitle endこれはこのままでいいと思います
※ コードは載せませんが、ここでアソシエーションを組んでいる相手方には、has_many :reviewsを書いてくださいrouteを設定
config/routes.rbresources :maintitles do # 省略 resources :stories do post 'like_review' delete 'unlike_review' #まだ実装できてません・・ resources :comments, only: [:create] end endmaintitlesにstoriesをネストさせ、storiewコントローラ内に
- like_review
- unlike_review
のアクションを設定します(後述)
ネストさせる意味は、reviewテーブルにmaintitle_idとstory_idを保存するためにやってますcontrollerを設定
app/controllers/stories.controller.rbbefore_action :set_maintitle, only: [:new, :create, :show, :like_review, # 省略] before_action :set_story, only: [:show, # 省略] //////////////////////////////////////////////////////// def show #set_maintitle #set_story @user = User.find(@story.user_id) if user_signed_in? @review = Review.find_by(story_id: @story.id, user_id: current_user.id) if @review != nil gon.my_review_count = @review.review + 1 else @review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0) gon.my_review_count = @review.review + 1 end end @reviews = Review.where(story_id: @story.id) @a = [] @reviews.each do |review| p = review.review @a.push(p) end @review_all_count = @a.sum gon.review_all_count = @a.sum + 1 end //////////////////////////////////////////////////////// def like_review # set_maintitle @story = Story.find(params[:story_id]) @review = Review.find_by(story_id: @story.id, user_id: current_user.id) p = @review.review + 1 @review.update(review: p) end //////////////////////////////////////////////////////// private def set_maintitle @maintitle = Maintitle.find(params[:maintitle_id]) end def set_story @story = Story.find(params[:id]) endごちゃごちゃしててすいません・・
ここから実装についてのポイントになってきますまず、before_actionで、set_maintitleとset_storyをshowアクションに反映させます
で、ログインしていれば評価ができる仕様にしてますので、
if user_signed_in?を使用します
@review = Review.find_by(story_id: @story.id, user_id: current_user.id)これで、現在閲覧している小説の内容のidと、自分のidを持ったreviewテーブルを探して取得します
すでに訪れていた場合はデータがありますが(後述)、初めて訪れた場合はもちろんnilとなりますif @review != nil gon.my_review_count = @review.review + 1 else @review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0) gon.my_review_count = @review.review + 1 endこれで、すでに訪れていた場合、gon.my_review_countというjsに渡す変数に、現在のreviewの数に+1させた数字を代入し、初めて訪れた場合はreviewに0を入れてcreateし、さらに前記同様変数に+1させた数字を代入します(後述)
@reviews = Review.where(story_id: @story.id) @a = [] @reviews.each do |review| p = review.review @a.push(p) end @review_all_count = @a.sum gon.review_all_count = @a.sum + 1この部分はstoryに対する各userの総評価数を取得する記述です
変数@reviewsに、whereでReviewテーブルから@story.idに一致するテーブルを全て取得し代入します
ここで、取得したデータからreviewカラムのデータを格納する空の配列@aを定義しておきます
whereで取得すると、配列でデータが取得されるので、eachで回し、reviewカラムのデータを抽出し、変数pへ代入し、空配列@aへpushにより格納していきます
そして、変数@review_all_countへ@aに入ったreviewデータの合計を.sumを用いて計算し、代入します
そして、jsへデータを渡すため、変数gon.review_all_countへ@a.sum + 1と、総評価数に+1をして代入しますlike_reviewアクションを定義する
def like_review # set_maintitle @story = Story.find(params[:story_id]) @review = Review.find_by(story_id: @story.id, user_id: current_user.id) p = @review.review + 1 @review.update(review: p) endこのアクションで、reviewカラムにアクション毎(クリック毎)に+1させていきます
まず、現在のstoryのデータを取得し、変数@storyへ代入します
そして、変数@reviewへReviewテーブルからstory_id,user_idで条件指定したテーブルを取得します
変数pへ、取得したReviewテーブルのreviewカラムのデータへ+1したデータを代入します
最後に、そのテーブルのreviewカラムをアップデートします
・・・長くなってきたので一度背伸びでもしてリフレッシュしてください
次いきます
viewを作成する
<%=link_to maintitle_story_like_review_path(@maintitle, @story), method: :post, remote: :true, class: :like_review_btn do%>viewは好みで作成していただくとして、大事なのは⬆️の一文です
link_toでlike_reviewアクションを実行します(pathはrake routesで確認してください)
このとき「remote: :true」を記述することにより、ajaxにより非同期でアクションが実行されます
よって、データ上の処理はこの時点でアクション毎に+1されていきます
(自分はまだajaxの仕組みをよく理解していませんので、詳しく知りたい方は別の記事等ググって調べてみてください)また、showに初めて訪れた際、reviewカラムに0を入れてcreateすると前述しましたが、初めて訪れた直後というのは、何もデータが作成されていません
初めて訪れたときの処理の流れを説明すると、
- showに訪れる
- createされる
という順番のため、createしたデータを渡したくてもエラーになります
そのため、
<%if @review.present?%> <i class="my_count"> <%=@review.review%> </i> <%else%> <i class="my_count"> 0 </i> <%end%>と、@reviewにデータが入っていない条件分岐を定義してあげて、エラーを回避&初期のreviewのデータ「0」を表示させなければなりません
jsでアクション毎に数字を表示する
app/assets/javascripts/like_review.jsfunction appendFaThoumbsOUpMore(my_review_count) { var fa_thumbs_o_up_more = $(".review_my_count_area"); var my_count_more = `<i class="my_count"> ${my_review_count} </i>` fa_thumbs_o_up_more.append(my_count_more); }; function appendFaThoumbsOUpAll(all_review_count) { var fa_thumbs_o_up_all = $(".review_all_count_area"); var all_count = `<i class="all_count"> ${all_review_count} </i>` fa_thumbs_o_up_all.append(all_count); }; $(function() { $(document).on("ajax:success", ".like_review_btn", function(e) { e.preventDefault(); $(".my_count").remove(); $(".all_count").remove(); var my_review_count = gon.my_review_count++ ; var all_review_count = gon.review_all_count++ ; appendFaThoumbsOUpMore(my_review_count); appendFaThoumbsOUpAll(all_review_count); }); });さて、最後の説明になります
まず、jsの大まかな処理の流れとして、
- like_reviewアクションが実行されたら(ajax通信が行われたら)イベント発火
- 古いデータの入ったhtml要素を排除
- 新しいデータの入ったhtml要素を挿入
という流れになります
まず、上段のhtmlは置いておいて、下段を見ていきます
$(document).on("ajax:success", ".like_review_btn", function(e) {この記述は「like_review_btn」というクラスを持ったhtml要素がajax通信を成功したら処理を行うという意味になります
$(".my_count").remove(); $(".all_count").remove();この記述で、それぞれのクラスを持ったhtml要素を排除します
var my_review_count = gon.my_review_count++ ; var all_review_count = gon.review_all_count++ ;これは、先ほどgonで定義した変数に+1したものを、それぞれ変数へ代入するものとなっています
ここで、controllerで定義したgonの変数について説明します
controllerでなぜ+1したかというと、データの表示はあくまで、「アクションが実行された後のデータ」を表示しなければなりません
なので、jsの++のみでは、1度目のアクションでの表示が「showに訪れた時のデータ」が表示されてしまいます
よって、コントローラで(jsでもできると思いますが)+1してあげることで、1度目のアクションでの表示がちゃんと+1されて表示することができるのです
(console.log等で確認してみるとわかります)appendFaThoumbsOUpMore(my_review_count); appendFaThoumbsOUpAll(all_review_count); /////////////////////////////////////////////// function appendFaThoumbsOUpMore(my_review_count) { var fa_thumbs_o_up_more = $(".review_my_count_area"); var my_count_more = `<i class="my_count"> ${my_review_count} </i>` fa_thumbs_o_up_more.append(my_count_more); }; function appendFaThoumbsOUpAll(all_review_count) { var fa_thumbs_o_up_all = $(".review_all_count_area"); var all_count = `<i class="all_count"> ${all_review_count} </i>` fa_thumbs_o_up_all.append(all_count); };ここでは、先ほどremoveで排除したhtml要素へ、データを更新したhtml要素を挿入する記述となっています
以上で説明を終了します!!
長々読んでいただきありがとうございました文章書くの苦手なので非常にわかりにくかったかとは思いますが、これでとりあえずは実装できます
また、これで「評価の高い投稿」や、「自分が評価した投稿」等の検索や並び替えなども可能となります(と思う)また、動画ではよくわかりませんが、開発環境ではクリック後の数字の反映に少し時間がかかり、連打するとたまにデータが更新されないなどありますが、本番環境で試したところかなりサクサク表示され、連打しても問題なくデータも更新されていました
あと、今回の実装では最初、「1度目のアクションがcreateアクション」「2度目からupdateアクション」という2段構えでやっていたのですが、どうにもうまく行かなかったため、「showへ訪れたらcreateアクション」するという方法にシフトチェンジしました
なので、評価していなくてもデータが作成されるため、本当にこの実装の仕方でいいのか疑問が残っています
ここは要検討ですねまた、冒頭でも言いましたが、自分は初学者ですので、もっと簡単にできる等あれば、是非教えていただけるとありがたく感じます
ということで失礼します
- 投稿日:2019-11-27T14:49:12+09:00
rails_クリックでカウントアップとデータの保存
タイトルのように、クリックするごとに数字をカウントアップし、非同期でデータを保存する方法を紹介します
イメージとしては、動画を確認してください⬇️
— Kamotetu (@Kamotetu2) November 27, 2019自分もプログラミング初学者なので、もっとやり方あると思いますが、ググっても良い記事がなく、同じような実装に悩まれている方が多かったので、「とりあえず実装できる」レベルですが、誰かの参考になればと思います
あと、ここで例として紹介しているアプリケーションの内容は小説を定義(maintitle)し、その小説の内容(story)を随時アップしていくという内容になっていますので、なんとなくイメージしていただくと記事の内容が理解しやすいかなと思います
また、マイグレーションファイルの作成や、コントローラの作成など、ここで紹介していない部分の実装等については、別の方の記事等を参考にしてください
それではちょっと長くなりますが紹介していきます
- ruby 2.5.1
- rails 5.2.3
使用しているgem
- 'gon'
- 'device'
- 'jquery-rails'
○○_create_reviews.rbclass CreateReviews < ActiveRecord::Migration[5.2] def change create_table :reviews do |t| t.integer :user_id t.integer :story_id t.integer :maintitle_id t.integer :review t.timestamps end end endapp/models/review.rbclass Review < ApplicationRecord belongs_to :user belongs_to :story belongs_to :maintitle endconfig/routes.rbresources :maintitles do # 省略 resources :stories do post 'like_review' delete 'unlike_review' #まだ実装できてません・・ resources :comments, only: [:create] end endapp/controllers/stories.controller.rbbefore_action :set_maintitle, only: [:new, :create, :show, :like_review, # 省略] before_action :set_story, only: [:show, # 省略] //////////////////////////////////////////////////////// def show #set_maintitle #set_story @user = User.find(@story.user_id) if user_signed_in? @review = Review.find_by(story_id: @story.id, user_id: current_user.id) if @review != nil gon.my_review_count = @review.review + 1 else @review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0) gon.my_review_count = @review.review + 1 end end @reviews = Review.where(story_id: @story.id) @a = [] @reviews.each do |review| p = review.review @a.push(p) end @review_all_count = @a.sum gon.review_all_count = @a.sum + 1 end //////////////////////////////////////////////////////// def like_review # set_maintitle @story = Story.find(params[:story_id]) @review = Review.find_by(story_id: @story.id, user_id: current_user.id) p = @review.review + 1 @review.update(review: p) end //////////////////////////////////////////////////////// private def set_maintitle @maintitle = Maintitle.find(params[:maintitle_id]) end def set_story @story = Story.find(params[:id]) endapp/views/stories/show.html.erb# 省略 <%if @story.user_id != current_user.id%> <div class="like_review_area"> <%=link_to maintitle_story_like_review_path(@maintitle, @story), method: :post, remote: :true, class: :like_review_btn do%> <div class="like_review_link"> <div class="like_review", id="<%=@maintitle.id%>"> 面白かった数だけここをクリック! </div> </div> <%end%> </div> <%end%> <%if user_signed_in?%> <%if @story.user_id != current_user.id%> <div class="show_review_my_count_area"> あなたの評価: <i class="fa fa-thumbs-o-up "> </i> <i class="review_my_count_area"> <%if @review.present?%> <i class="my_count"> <%=@review.review%> </i> <%else%> <i class="my_count"> 0 </i> <%end%> </i> </div> <%end%> <%end%>app/assets/javascripts/like_review.jsfunction appendFaThoumbsOUpMore(my_review_count) { var fa_thumbs_o_up_more = $(".review_my_count_area"); var my_count_more = `<i class="my_count"> ${my_review_count} </i>` fa_thumbs_o_up_more.append(my_count_more); }; function appendFaThoumbsOUpAll(all_review_count) { var fa_thumbs_o_up_all = $(".review_all_count_area"); var all_count = `<i class="all_count"> ${all_review_count} </i>` fa_thumbs_o_up_all.append(all_count); }; $(function() { $(document).on("ajax:success", ".like_review_btn", function(e) { e.preventDefault(); $(".my_count").remove(); $(".all_count").remove(); var my_review_count = gon.my_review_count++ ; var all_review_count = gon.review_all_count++ ; appendFaThoumbsOUpMore(my_review_count); appendFaThoumbsOUpAll(all_review_count); }); });ずらずら記載してすいません
ということでポイントを説明していきます
reviewテーブルを作成
自分は、誰がどの投稿に対して評価をつけたかわかるように、
○○_create_reviews.rbclass CreateReviews < ActiveRecord::Migration[5.2] def change create_table :reviews do |t| t.integer :user_id t.integer :story_id t.integer :maintitle_id t.integer :review t.timestamps end end endとしましたが、単に評価の数だけならuser_idとかいらないと思います
あとそれぞれにnull: false foreign_key: trueもつけた方がいいかもですね
modelにアソシエーションを定義
app/models/review.rbclass Review < ApplicationRecord belongs_to :user belongs_to :story belongs_to :maintitle endこれはこのままでいいと思います
※ コードは載せませんが、ここでアソシエーションを組んでいる相手方には、has_many :reviewsを書いてくださいrouteを設定
config/routes.rbresources :maintitles do # 省略 resources :stories do post 'like_review' delete 'unlike_review' #まだ実装できてません・・ resources :comments, only: [:create] end endmaintitlesにstoriesをネストさせ、storiesコントローラ内に
- like_review
- unlike_review
のアクションを設定します(後述)
ネストさせる意味は、reviewテーブルにmaintitle_idとstory_idを保存するためにやってますcontrollerを設定
app/controllers/stories.controller.rbbefore_action :set_maintitle, only: [:new, :create, :show, :like_review, # 省略] before_action :set_story, only: [:show, # 省略] //////////////////////////////////////////////////////// def show #set_maintitle #set_story @user = User.find(@story.user_id) if user_signed_in? @review = Review.find_by(story_id: @story.id, user_id: current_user.id) if @review != nil gon.my_review_count = @review.review + 1 else @review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0) gon.my_review_count = @review.review + 1 end end @reviews = Review.where(story_id: @story.id) @a = [] @reviews.each do |review| p = review.review @a.push(p) end @review_all_count = @a.sum gon.review_all_count = @a.sum + 1 end //////////////////////////////////////////////////////// def like_review # set_maintitle @story = Story.find(params[:story_id]) @review = Review.find_by(story_id: @story.id, user_id: current_user.id) p = @review.review + 1 @review.update(review: p) end //////////////////////////////////////////////////////// private def set_maintitle @maintitle = Maintitle.find(params[:maintitle_id]) end def set_story @story = Story.find(params[:id]) endごちゃごちゃしててすいません・・
ここから実装についてのポイントになってきますまず、before_actionで、set_maintitleとset_storyをshowアクションに反映させます
で、ログインしていれば評価ができる仕様にしてますので、
if user_signed_in?を使用します
@review = Review.find_by(story_id: @story.id, user_id: current_user.id)これで、現在閲覧している小説の内容のidと、自分のidを持ったreviewテーブルを探して取得します
すでに訪れていた場合はデータがありますが(後述)、初めて訪れた場合はもちろんnilとなりますif @review != nil gon.my_review_count = @review.review + 1 else @review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0) gon.my_review_count = @review.review + 1 endこれで、すでに訪れていた場合、gon.my_review_countというjsに渡す変数に、現在のreviewの数に+1させた数字を代入し、初めて訪れた場合はreviewに0を入れてcreateし、さらに前記同様変数に+1させた数字を代入します(後述)
@reviews = Review.where(story_id: @story.id) @a = [] @reviews.each do |review| p = review.review @a.push(p) end @review_all_count = @a.sum gon.review_all_count = @a.sum + 1この部分はstoryに対する各userの総評価数を取得する記述です
変数@reviewsに、whereでReviewテーブルから@story.idに一致するテーブルを全て取得し代入します
ここで、取得したデータからreviewカラムのデータを格納する空の配列@aを定義しておきます
whereで取得すると、配列でデータが取得されるので、eachで回し、reviewカラムのデータを抽出し、変数pへ代入し、空配列@aへpushにより格納していきます
そして、変数@review_all_countへ@aに入ったreviewデータの合計を.sumを用いて計算し、代入します
そして、jsへデータを渡すため、変数gon.review_all_countへ@a.sum + 1と、総評価数に+1をして代入しますlike_reviewアクションを定義する
def like_review # set_maintitle @story = Story.find(params[:story_id]) @review = Review.find_by(story_id: @story.id, user_id: current_user.id) p = @review.review + 1 @review.update(review: p) endこのアクションで、reviewカラムにアクション毎(クリック毎)に+1させていきます
まず、現在のstoryのデータを取得し、変数@storyへ代入します
そして、変数@reviewへReviewテーブルからstory_id,user_idで条件指定したテーブルを取得します
変数pへ、取得したReviewテーブルのreviewカラムのデータへ+1したデータを代入します
最後に、そのテーブルのreviewカラムをアップデートします
・・・長くなってきたので一度背伸びでもしてリフレッシュしてください
次いきます
viewを作成する
<%=link_to maintitle_story_like_review_path(@maintitle, @story), method: :post, remote: :true, class: :like_review_btn do%>viewは好みで作成していただくとして、大事なのは⬆️の一文です
link_toでlike_reviewアクションを実行します(pathはrake routesで確認してください)
このとき「remote: :true」を記述することにより、ajaxにより非同期でアクションが実行されます
よって、データ上の処理はこの時点でアクション毎に+1されていきます
(自分はまだajaxの仕組みをよく理解していませんので、詳しく知りたい方は別の記事等ググって調べてみてください)また、showに初めて訪れた際、reviewカラムに0を入れてcreateすると前述しましたが、初めて訪れた直後というのは、何もデータが作成されていません
初めて訪れたときの処理の流れを説明すると、
- showに訪れる
- createされる
という順番のため、createしたデータを渡したくてもエラーになります
そのため、
<%if @review.present?%> <i class="my_count"> <%=@review.review%> </i> <%else%> <i class="my_count"> 0 </i> <%end%>と、@reviewにデータが入っていない条件分岐を定義してあげて、エラーを回避&初期のreviewのデータ「0」を表示させなければなりません
jsでアクション毎に数字を表示する
app/assets/javascripts/like_review.jsfunction appendFaThoumbsOUpMore(my_review_count) { var fa_thumbs_o_up_more = $(".review_my_count_area"); var my_count_more = `<i class="my_count"> ${my_review_count} </i>` fa_thumbs_o_up_more.append(my_count_more); }; function appendFaThoumbsOUpAll(all_review_count) { var fa_thumbs_o_up_all = $(".review_all_count_area"); var all_count = `<i class="all_count"> ${all_review_count} </i>` fa_thumbs_o_up_all.append(all_count); }; $(function() { $(document).on("ajax:success", ".like_review_btn", function(e) { e.preventDefault(); $(".my_count").remove(); $(".all_count").remove(); var my_review_count = gon.my_review_count++ ; var all_review_count = gon.review_all_count++ ; appendFaThoumbsOUpMore(my_review_count); appendFaThoumbsOUpAll(all_review_count); }); });さて、最後の説明になります
まず、jsの大まかな処理の流れとして、
- like_reviewアクションが実行されたら(ajax通信が行われたら)イベント発火
- 古いデータの入ったhtml要素を排除
- 新しいデータの入ったhtml要素を挿入
という流れになります
まず、上段のhtmlは置いておいて、下段を見ていきます
$(document).on("ajax:success", ".like_review_btn", function(e) {この記述は「like_review_btn」というクラスを持ったhtml要素がajax通信を成功したら処理を行うという意味になります
$(".my_count").remove(); $(".all_count").remove();この記述で、それぞれのクラスを持ったhtml要素を排除します
var my_review_count = gon.my_review_count++ ; var all_review_count = gon.review_all_count++ ;これは、先ほどgonで定義した変数に+1したものを、それぞれ変数へ代入するものとなっています
ここで、controllerで定義したgonの変数について説明します
controllerでなぜ+1したかというと、データの表示はあくまで、「アクションが実行された後のデータ」を表示しなければなりません
なので、jsの++のみでは、1度目のアクションでの表示が「showに訪れた時のデータ」が表示されてしまいます
よって、コントローラで(jsでもできると思いますが)+1してあげることで、1度目のアクションでの表示がちゃんと+1されて表示することができるのです
(console.log等で確認してみるとわかります)appendFaThoumbsOUpMore(my_review_count); appendFaThoumbsOUpAll(all_review_count); /////////////////////////////////////////////// function appendFaThoumbsOUpMore(my_review_count) { var fa_thumbs_o_up_more = $(".review_my_count_area"); var my_count_more = `<i class="my_count"> ${my_review_count} </i>` fa_thumbs_o_up_more.append(my_count_more); }; function appendFaThoumbsOUpAll(all_review_count) { var fa_thumbs_o_up_all = $(".review_all_count_area"); var all_count = `<i class="all_count"> ${all_review_count} </i>` fa_thumbs_o_up_all.append(all_count); };ここでは、先ほどremoveで排除したhtml要素へ、データを更新したhtml要素を挿入する記述となっています
以上で説明を終了します!!
長々読んでいただきありがとうございました文章書くの苦手なので非常にわかりにくかったかとは思いますが、これでとりあえずは実装できます
また、これで「評価の高い投稿」や、「自分が評価した投稿」等の検索や並び替えなども可能となります(と思う)また、動画ではよくわかりませんが、開発環境ではクリック後の数字の反映に少し時間がかかり、連打するとたまにデータが更新されないなどありますが、本番環境で試したところかなりサクサク表示され、連打しても問題なくデータも更新されていました
あと、今回の実装では最初、「1度目のアクションがcreateアクション」「2度目からupdateアクション」という2段構えでやっていたのですが、どうにもうまく行かなかったため、「showへ訪れたらcreateアクション」するという方法にシフトチェンジしました
なので、評価していなくてもデータが作成されるため、本当にこの実装の仕方でいいのか疑問が残っています
ここは要検討ですねまた、冒頭でも言いましたが、自分は初学者ですので、もっと簡単にできる等あれば、是非教えていただけるとありがたく感じます
ということで失礼します
- 投稿日:2019-11-27T12:36:33+09:00
Rails6 のちょい足しな新機能を試す108(multi-db abort_if_pending_migrations編)
はじめに
Rails 6 に追加された新機能を試す第108段。 今回は、 multi-db abort_if_pending_migrations 編です。
Rails 6 では、rails db:abort_if_pending_migrations
が multi database に対応しました。Ruby 2.6.5, Rails 6.0.0 で確認しました。 (Rails 6.0.1 がリリースされていますが、確認した時点ではリリースされていませんでした。)
$ rails --version Rails 6.0.0今回は、簡単なスクリプトを作って確認します。
Rails プロジェクトを作成する
$ rails new rails_sandbox $ cd rails_sandbox今回は複数のDBを作って、それぞれ、 User と Book のCRUD を作ってから、
rails db:abort_if_pending_migrations
を実行して確認します。config/database.yml を編集する
config/database.yml
を編集して複数データベースにします。config/database.yml... development: backbone: <<: *default database: backbone_development library: <<: *default database: library_development migrations_paths: db/library_migrate ...User の CRUD を作成する
User
の CRUD を作成します。$ bin/rails g scaffold User name
Book の CRUD を作成する
Book
の CRUD を作成します。$ bin/rails g scaffold Book title --db libraryBook モデルを編集する
データベースの接続先を
library
に変更します。app/models/book.rbclass Book < ApplicationRecord connects_to database: { writing: :library, reading: :library } endマイグレーションを実行する
$ bin/rails db:create db:migrate
rails server
を実行する別に実行しなくても良いのですが、別の事象に遭遇したので、ここで実行します。
$ bin/rails s
マイグレーションを作成する
rails server を実行したのとは、別のコンソールから、books に published_at カラムを追加するマイグレーションを作成します。
$ bin/rails g migration add_published_at_to_books published_at:datetime --db librarydb:abort_if_pending_migrations を実行する
rails db:abort_if_pending_migrations
を実行すると、ちゃんとrails db:migrate
を実行しろとメッセージが出ます。$ rails db:abort_if_pending_migrations You have 1 pending migration: 20191022023755 AddPublishedAtToBooks Run `rails db:migrate` to update your database then try again.ブラウザでアクセスする
どういう訳か、http://localhost:3000/users または、 http://localhost:3000/books にブラウザからアクセスしても
ActiveRecord::PendingMigrationError
になりませんでした。
予想と違ったので、 Issue として報告 しておきました。試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try108_multi_db_pending参考情報
- 投稿日:2019-11-27T12:19:08+09:00
投稿に対してのコメントの削除した際にエラー ActiveRecord::RecordNotFound
投稿に対してコメントできる機能を実装しました。
コメントは投稿詳細ページでコメントできるようにしています。
そのコメントを削除する際にエラーが出ます。
ActiveRecord::RecordNotFound in TweetsController#destroy
Couldn't find Tweet with 'id'=27
とでます。
tweets.controller.rbのdestroyアクションに飛んでいる状態です
本来はcomments.controller.rbのdestroyアクションに飛ばしたいです。この削除ボタンのlink先のurlが悪いのでしょうか?
= link_to "削除","/tweets/#{comment.id}", method: :delete, class: "image-delete"教えてくださいよろしくお願いします。
ファイル名<comments_controller.rb> class CommentsController < ApplicationController def create @comment = Comment.create(text: comment_params[:text], tweet_id: comment_params[:tweet_id], user_id: current_user.id) respond_to do |format| format.html { redirect_to tweet_path(params[:tweet_id]) } format.json end end def destroy comment = Comment.find(params[:id]) comment.destroy end private def comment_params params.require(:comment).permit(:text).merge(user_id: current_user.id, tweet_id: params[:tweet_id]) end endファイル名<views/comments/_comment.html.haml> .comments %h4 <コメント一覧> - if @comments - @comments.each do |comment| %p %strong = link_to comment.user.nickname, "/users/#{comment.user_id}" : = comment.text - if user_signed_in? && current_user.id == comment.user_id = link_to "削除","/tweets/#{comment.id}", method: :delete, class: "image-delete"ファイル名<routes.rb> resources :tweets do resources :comments, only: [:create, :destroy] end
- 投稿日:2019-11-27T08:54:40+09:00
【Rails on Docker】Modelの全件取得(all)ができなくなって困った
Problem
rails console
でとあるモデルをall
で全件取得しようとしたところ以下のエラーに遭遇。[1] pry(main)> User.all Member Load (9.3ms) SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC less: unrecognized option: X BusyBox v1.30.1 (2019-10-26 11:23:07 UTC) multi-call binary. Usage: less [-EFIMmNSRh~] [FILE]... 1 FROM ruby:2.6.5-alpine3.10 View FILE (or stdin) one screenful at a time -E Quit once the end of a file is reached -F Quit if entire file fits on first screen -I Ignore case in all searches -M,-m Display status line with line numbers and percentage through the file -N Prefix line number to each line -S Truncate long lines -R Remove color escape codes in input -~ Suppress ~s displayed past EOFなんかできるモデルとできないモデルがあって困った。
なんとなく件数が少ないモデルは見えるけど多いモデルはエラーになる。Trouble Shooting
結論からいえば、Alpine LinuxをベースとしてDocker上でRailsを動かしていたのですが、Alpine Linuxには
less
が標準装備されていないためこうなるらしい。
hirb
とか使っているからpager機能がついているのかな...その時less使ってるのかな...
まぁとりあえず復旧だ。DockerfileRUN apk add --update --no-cache lessdocker-compose buildエラー出なくなった。
Reference
- 投稿日:2019-11-27T08:20:17+09:00
N+1問題を解決ゾロリ
【結論】includesを使う
データベースにアクセスする回数を減らそう
データを取ってくる際にSQLというものが発行されます。
アソシエーションを組んでいる場合、子要素のデータを取ってくる時もあるでしょう。
例えばこんな時index.html.haml- @tweets.each do |tweet| = tweet.text = tweet.user.nicknametweetsテーブルとusersテーブルからデータを取ってきてます
この時SQLは通常(tweetsテーブル)+1回(usersテーブル)発行されてしまいます。SQLが多いと処理が重くなるので、1回で関連するデータを一気に持ってきちゃおうぜ!って話です。
UserモデルとTweetモデルが以下のアソシエーションを組んでいるとします。
user.rbhas_many :tweetstweet.rbbelongs_to :user
【tweetsコントローラーを編集】
includes (:モデル名)
これで一気に関連データまで取ってくることができます。tweets_controller.rbdef index @tweets = Tweet.includes(:user) #allは省略可 endこれでN+1問題はバッチリ解決です!
ではまた!
- 投稿日:2019-11-27T08:18:39+09:00
Active Storageで画像アップロード機能を実装や!
【その1】インストール
※Rails 5.2から搭載されています。
ターミナルrails active_storage:install rails db:migrate【その2】モデルに設定を追記
1つのファイルなら
has_one_attached :カラム名
複数ファイルならhas_many_attached :カラム名
モデルhas_one_attached :avatar has_many_attached :images実際にテーブルにカラムを追加する必要はありません。
【その3】表示する
ビューファイルuser.avatarこれを好きなところへ書いてください。
もちろんimage_tagなどを添えて。
超参考
ではまた!
- 投稿日:2019-11-27T08:16:19+09:00
before_action
いつ使うの
before_actionを使えば、すべてのアクションのが実行される前に指定したメソッドを呼び、共通の処理を実行することができます。
使い方
before_action :メソッド名これをコントローラーの一番上の書きます。
オプションとしてonlyとexceptがあります。only: [:アクションA]
→アクションAのときのみexcept: [:アクションB]
→アクションB以外のとき例
tweets_controller.rbclass TweetsController < ApplicationController before_action :move_to_index, except: [:index] def index @tweets = Tweet.all end def new end 中略 def move_to_index redirect_to action: :index unless user_signed_in? end endindexアクション以外のとき、まず初めにmove_to_indexを実行します。
move_to_indexはログインしていない場合に、indexアクションへ遷移させるというものです。
これで、ログインしていないと新規投稿などができなくなります。
ちなみにdeviseがインストールされている場合は、
authenticate_user!というヘルパーメソッドを使って省略できます。tweets_controller.rbclass TweetsController < ApplicationController before_action :authenticate_user!, except: [:index] def index end def new end 中略 end
deviseのヘルパーメソッドはこちらへ
ではまた!
- 投稿日:2019-11-27T01:29:35+09:00
【Rails】統合テストによるリンクのテスト【Rails Tutorial 5章まとめ】
link_toと名前付きルートで作ったリンクが正常に表示され、機能しているかどうかを統合テストで確認する。
統合テストはアプリケーションの動作をシミュレートするために使用される。$ rails generate integration_test site_layoutテストの手順
①ルートURL (Homeページ) にGETリクエストを送る
②正しいページテンプレートが描画されているかどうか確かめる
③Home、Help、About、Contactの各ページへのリンクが正しく動くか確かめるtest/integration/site_layout_test.rbtest "layout links" do get root_path assert_template 'static_pages/home' assert_select "a[href=?]", root_path, count: 2 assert_select "a[href=?]", help_path assert_select "a[href=?]", about_path assert_select "a[href=?]", contact_path endassert_select "a[href=?]", about_pathはlink_toによって生成されたaタグを調べている。
?の部分に第二引数のパスが代入される。
countオプションによってリンクの個数も調べることができる。assert_selectについては別記事にまとめたいと思っているが、分かりやすいサイトを見つけたのでここにメモしておく。
assert_selectの使い方
https://zariganitosh.hatenablog.jp/entry/20080405/1207455670これ以降、レイアウトに新規のアクションとそのリンクを作成した場合は、このテストに書いていくことになる。
- 投稿日:2019-11-27T01:04:04+09:00
【Rails】ルーティングと名前付きルート【Rails Tutorial 5章まとめ】
ルートURL
rootを使い、ルートURLを指定する。
config/routes.rbroot 'static_pages#home'ルートURLは、root_pathまたはroot_urlで呼び出せる(名前付きルート)。
root_path -> '/' root_url -> 'http://www.example.com/'前者はルートURL以下の文字を、後者はURLの全文を表示する。
基本的には前者を使い、後者はリダイレクトの場合に使用する。そのほかのルーティング
config/routes.rbget 'static_pages/help' get '/help', to: 'static_pages#help'前者の場合static_pagesコントローラのhelpアクションに自動で繋がる。
名前付きルートはstatic_pages_help_pathのようになる。とはいえ長いしURLの自由度が無いので基本的には後者のようにして、任意のURLに対してコントローラとアクションを指定する。
名前付きルートはhelp_pathやhelp_urlのようになる。コントローラのテストも修正しておく。
test/controllers/static_pages_controller_test.rbtest "should get home" do get root_path assert_response :success assert_select "title", "Ruby on Rails Tutorial Sample App" end test "should get help" do get help_path assert_response :success assert_select "title", "Help | Ruby on Rails Tutorial Sample App" end名前付きルートを任意の名前にする
as:オプションを使うと名前付きルートの名前を変えられる。
config/routes.rbget '/help', to: 'static_pages#help', as: 'helf'
- 投稿日:2019-11-27T00:41:06+09:00
【Rails】レイアウトの作成とパーシャル【Rails Tutorial 5章まとめ】
アプリケーションのレイアウトを作成する。
app/views/layouts/application.html.erb<!DOCTYPE html> <html> <head> <title><%= full_title(yield(:title)) %></title> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> <!--[if lt IE 9]> <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js"> </script> <![endif]--> </head> <body> <header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", '#', id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", '#' %></li> <li><%= link_to "Help", '#' %></li> <li><%= link_to "Log in", '#' %></li> </ul> </nav> </div> </header> <div class="container"> <%= yield %> </div> </body> </html><!--[if lt IE 9]>はIE9未満のブラウザ用(いまだにIEを使う人間がいるのかどうかは定かではない)。
navタグはリンクの一覧などの「主要なナビゲーション」に使うらしい。link_toはRailsヘルパーであり、aタグを生成する。
第一引数にリンクテキスト、第二引数にURLを指定する。
URLには名前付きルートが使える。
classを指定する場合は、第三引数にclass:をキーにしたハッシュの形で指定する(idも同じ)。link_toのリンクテキストには、image_tagを使用することで画像を指定できる。
app/views/static_pages/home.html.erb<%= link_to image_tag("rails.png", alt: "Rails logo"), 'http://rubyonrails.org/' %>画像(rails.png)はapp/assets/images/に置く。
altは画像が表示されない場合に代わりに表示される文字列である。パーシャル
パーシャルによって、レイアウトのコードをそのまとまり毎に別のファイルに分割する。
app/views/layouts/application.html.erb<!DOCTYPE html> <html> <%= render 'layouts/head' %> <body> <%= render 'layouts/header' %> <div class="container"> <%= yield %> <%= render 'layouts/footer' %> </div> </body> </html><header>...</header>の部分を<%= render 'layouts/header' %>に置換している。
<header>とその中身は、パーシャル(app/views/layouts/_header.html.erb)を作成してそこに移す。
パーシャルのファイル名にはアンダーバーをつける。
renderで呼び出す場合は、アンダーバーはつけない。
- 投稿日:2019-11-27T00:30:22+09:00
devise で認証キーを email から username に変更する
はじめに
手元に同志が作った Rails アプリがありましたとさ。
これを流用・シュリンクして別の目的のアプリに改変することにしましたとさ。
認証には devise が利用されていましたとさ。定期。なんか手元で試したら、ネットで見かける改変手順より全然少なかったので、ちょっと整理しておこうかと思った次第です。
結論
核としては、モデルにフィールドを追加して
config/initializers/devise.rb
のconfig.authentication_keys
を変更するだけでした。diff --git a/db/migrate/20190123123456_add_username_to_users.rb b/db/migrate/20190123123456_add_username_to_users.rb new file mode 100644 index 0000000..803173d --- /dev/null +++ b/db/migrate/20190123123456_add_username_to_users.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddUsernameToUsers < ActiveRecord::Migration[5.2] + def change + add_column :users, :username, :string, default: '', null: false + end +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 4b5dfa4..7d993f6 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -44,7 +44,7 @@ Devise.setup do |config| # session. If you need permissions, you should implement that in a before filter. # You can also supply a hash where the value is a boolean determining whether # or not authentication should be aborted when the value is not present. - # config.authentication_keys = [:email] + config.authentication_keys = [:username] # Configure parameters from the request object used for authentication. Each entry # given should be a request method and it will automatically be passed to the @@ -56,12 +56,12 @@ Devise.setup do |config| # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [:email] + # config.case_insensitive_keys = [:email] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [:email] + # config.strip_whitespace_keys = [:email] # Tell if authentication through request.params is enabled. True by default. # It can be set to an array that will enable params authentication only for thedevise には Strong Parameters のデフォルトがある
ここを見ると幾つかの予約語が定義されています。
password
とかremember_me
とか。
そしてここでauthentication_keys
を参照しています。これらはユーザーが能動的に指定しなくても Strong Parameters として処理されます。
ところで
この変更をする前に作ったユーザーは、変更後、仮に username を追加できたとしても認証できないです。ご注意を。
おわりに
devise も黒魔術ですね。すごいなぁ。
- 投稿日:2019-11-27T00:07:36+09:00
【Rails】bootstrap導入【Rails Tutorial 5章まとめ】
Gemfileにbootstrapを記述。
Gemfile.rbgem 'bootstrap-sass', '3.3.7'$ bundle installしておく。
レイアウト用のcssを作って@importを使ってbootstrapを読み込む。app/assets/stylesheets/custom.scss@import "bootstrap-sprockets"; @import "bootstrap";