20191127のRailsに関する記事は29件です。

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-deploy

Compiled successfully !

build/ディレクトリにindex.htmlがあることも確認しました。

Nginxの設定

設定ファイルのrootbuild/ディレクトリにする

設定の読み込み

$ sudo systemctl reload nginx

その後、ドメインにアクセスし、アプリが表示されることを確認!

ついでに、今後新しい修正を反映するときはnpm run buildやunicornの再起動を忘れずに。

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

【Rails】メールアドレスの一意性の検証【Rails Tutorial 6章まとめ】

メールアドレスはユーザーごとに一意であり、重複してはならない。
既存のユーザーと同じメールアドレスを持つユーザーは無効となるように、テストを書く。

test/models/user_test.rb
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end

dupメソッドは、@userインスタンスをコピーする。
@userが保存された後は、duplicate_userは無効でなければならない。

emailのバリデーションにuniqunessオプションを追加して、重複できないようにする。

app/models/user.rb
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true

メールアドレスの大文字小文字

ところで、メールアドレスは大文字小文字を区別しないらしい。
つまり、foo@bar.comFOO@BAR.COMFoO@BAr.coMは同じである。

そこで、重複するユーザーのメールアドレスは、既存ユーザーのメールアドレスを大文字にして入れる。

test/models/user_test.rb
  test "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.rb
  validates :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_email
db/migrate/[timestamp]_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration[5.0]
  def change
    add_index :users, :email, unique: true
  end
end

uniqueオプションをtrueとすることで、インデックスの一意性が保たれる。
ところで、バリデーションでは"uniqueness"と名詞だったのに、インデックスでは"unique"と形容詞なのはなぜ?

マイグレーションを実行する。

rails db:migrate

ここで、テスト用のユーザーデータを入れておくfixtureファイルに、同じメールアドレスが設定されたユーザーが存在しているために、テストが失敗する。

test/fixtures/users.yml
one:
  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.Comfoo@example.comが別々の文字列だと解釈してしまうデータベースがあります」
とのこと。

データベースのアダプタとやらがイマイチよく分からないが、とにかくこれを解決するため、データベースにメールアドレスを保存する際には小文字に変換して保存するようにする。
before_saveメソッドを使う。

app/models/user.rb
class 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.rb
 test "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を除いてもテストに問題はなかった。

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

【Rails】Userモデルの基本的なバリデーションとテスト【Rails Tutorial 6章まとめ】

最初のモデルが有効であるかのテスト

生成したモデルの属性に値を与え、インスタンスが有効であるかをテストする(バリデーションはまだ無い)。

test/models/user_test.rb
require '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
end

assertは引数の論理値がtrueであることを確認する。
assert_notはその逆で、falseであることを確認する。

存在性の検証(presence)

name属性とemail属性は必ず存在していなければならない。
そこで、バリデーションを設定して各属性が空白の場合は有効でないようにする。

テスト駆動開発で進めていくので、バリデーションを設定する前に、バリデーションが無ければ失敗するテストを書く。

test/models/user_test.rb
  test "name should be present" do
    @user.name = "  "
    assert_not @user.valid?
  end

  test "email should be present" do
    @user.email = "  "
    assert_not @user.valid?
  end

nameとemailにそれぞれ空白を入れる。
バリデーションが無いので@user.valid?はtrueを返し、assert_notに反するため、テストは失敗する。

テストを成功させるために、Userモデルにバリデーションを設定する。

app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
end

テストをして、成功することを確認しておく。

長さの制限

nameとemailは長すぎないほうが良い。
長すぎるnameとemailを制限するバリデーションを、テストを書いてから設定する。

test/models/user_test.rb
  test "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?
  end

nameは50文字まで、emailはデータベースの文字列の限界である255文字までとする。
そこでnameに51文字、emailに256文字を入れておく。

app/models/user.rb
class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end

presenceオプションとlengthオプションはそれぞれをキーとする一つのハッシュだが、validatesメソッドの最後の引数なので、{}は無くてもよい。
lengthキーの値はハッシュなので、{}が必要である。

テストをして、成功することを確認しておく。

emailのフォーマットの検証

現在のままではemailにメールアドレス以外を入れても保存できてしまうので、フォーマットを制限する。

バリデーションの前に、有効なメールアドレスを確認するテストと、無効なメールアドレスを確認するテストを書く。

test/models/user_test.rb
  test "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.rb
class 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

テストをして、成功することを確認しておく。

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

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"}) %>

結果

279F7469-67DE-4214-876B-B45040438864.jpeg
※赤マルで囲った部分

要するに…

FontAwasomeのフォントはhtmlタグのclass属性にセレクタを指定すれば表示されるようになるので、
link_to の場合でもclassに指定してあげればFontAwasomeのフォントがリンク化するという話でした。
(最初はこれが分からなかった…)

class を囲っている中カッコは不要かもしれません。

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

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.erbapp/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 %>

これは気が付きにくい。

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

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.erbapp/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 %>

これは気が付きにくい。

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

【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メソッドは渡された引数を暗号化する

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

【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: home

paramsに含まれてる内容でYAMLという形式で書かれている

RESTの原則に従う場合、リソースへの参照はリソース名とユニークなIDを使うのが普通
例)
ユーザーをリソース
id=1のユーザーを参照する
⇢ /users/1というURLに対してGETリクエストを発行する

ここでいうshowアクションは暗黙のリクエストになる

Screen Shot 0001-11-27 at 12.43.49.png

app/views/users/show.html.erb
<%= @user.name %> , <%= @user.email %>
app/controllers/user_controller.rb
def show
  @user = User.find(params[:id])
end

ユーザーのid読み出しにはparamsを使用
Usersコントローラにリクエストが正常に送信されると
params[:id]の部分はユーザーidの1に置き換わる
params[:id]は文字列型の'1'だが
findメソッドでは自動的に整数型に変換される

Screen Shot 0001-11-27 at 13.06.18.png

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"}}
end

assert_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を導入した

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

特定パス以下のActionController::RoutingErrorでJSONを返却する

環境

Ruby 2.5.7
Rails 5.2 (ActionPack 5.2)

コード

lib/middleware/routing_error_response_json.rb
module 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
end
config/initializers/middleware.rb
require_relative "../../lib/middleware/routing_error_response_json"
::Rails.application.config.middleware.use YourNameSpace::Middleware::RoutingErrorResponseJson

実現できる事

↑の例であれば /api/v1 以下へのリクエストでRoutingErrorとなった場合にJSONを返却する事ができる。
WebとApiが同居しているアプリケーションなどで利用価値があるかもしれない。

最後に

もっと良い方法があれば教えて欲しいです。

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

クイズ:なぜバリデーションエラーになるでしょう

要約

  • 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.rb
class 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.rb
class ArticleTagRelation < ApplicationRecord
  belongs_to :article
  belongs_to :tag
end
article.rb
class Article < ApplicationRecord
  #Articlesテーブルから中間テーブルに対する関連付け
  has_many :article_tag_relations, dependent: :destroy
 #Articlesテーブルから中間テーブルを介してTagsテーブルへの関連付け
  has_many :tags, through: :article_tag_relations, dependent: :destroy
end
tag.rb
class 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.rb
Tag.create([
  { name: 'タグ1' },
  { name: 'タグ2' },
  { name: 'タグ3' },
  { name: 'タグ4' },
  { name: 'タグ5' }
])
$rails db:seed

スクリーンショット 2019-11-27 16.33.11.png

先ほど投入したタグデータのチェックボックスが表示されていますね。

コントローラーの作成

articles_controller.rb
 class 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-27 16.34.31.png

このように複数のタグをチェックした場合でも、表示できれば完成です。

最後に

今回はタグをチェックボックスで選択し、記事に表示するまでの機能を実装してみました。
ユーザーが自由にタグを追加できたり、タグで絞り込みなどを行えるようにするなど個人アプリを作る場合には、更に幅を広げてみると面白いかもしれません。

以上。

参考

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

これから作る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

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

これから作る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

6.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設定。アップして動いたら完成

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

rails db:migrateに失敗してStandardError: An error has occurred, all later migrations canceledが出た場合の対処方法

バージョン
Rails 5.0.1

rails 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

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

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" エラー

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

rails_クリックでカウントアップ&データの保存

タイトルのように、クリックするごとに数字をカウントアップし、非同期でデータを保存する方法を紹介します

イメージとしては、動画を確認してください⬇️

自分もプログラミング初学者なので、もっとやり方あると思いますが、ググっても良い記事がなく、同じような実装に悩まれている方が多かったので、「とりあえず実装できる」レベルですが、誰かの参考になればと思います

あと、ここで例として紹介しているアプリケーションの内容は小説を定義(maintitle)し、その小説の内容(story)を随時アップしていくという内容になっていますので、なんとなくイメージしていただくと記事の内容が理解しやすいかなと思います

また、マイグレーションファイルの作成や、コントローラの作成など、ここで紹介していない部分の実装等については、別の方の記事等を参考にしてください

それではちょっと長くなりますが紹介していきます

  • ruby 2.5.1
  • rails 5.2.3

使用しているgem

  • 'gon'
  • 'device'
  • 'jquery-rails'
○○_create_reviews.rb
class 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
app/models/review.rb
class Review < ApplicationRecord
  belongs_to :user
  belongs_to :story
  belongs_to :maintitle
end
config/routes.rb
resources :maintitles do
    # 省略
    resources :stories do
      post 'like_review'
      delete 'unlike_review' #まだ実装できてません・・
      resources :comments, only: [:create]
    end
  end
app/controllers/stories.controller.rb
before_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
app/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.js
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);
};

$(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.rb
class 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.rb
class Review < ApplicationRecord
  belongs_to :user
  belongs_to :story
  belongs_to :maintitle
end

これはこのままでいいと思います
※ コードは載せませんが、ここでアソシエーションを組んでいる相手方には、has_many :reviewsを書いてください

routeを設定

config/routes.rb
resources :maintitles do
    # 省略
    resources :stories do
      post 'like_review'
      delete 'unlike_review' #まだ実装できてません・・
      resources :comments, only: [:create]
    end
  end

maintitlesにstoriesをネストさせ、storiewコントローラ内に

  • like_review
  • unlike_review

のアクションを設定します(後述)
ネストさせる意味は、reviewテーブルにmaintitle_idとstory_idを保存するためにやってます

controllerを設定

app/controllers/stories.controller.rb
before_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.js
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);
};

$(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アクション」するという方法にシフトチェンジしました
なので、評価していなくてもデータが作成されるため、本当にこの実装の仕方でいいのか疑問が残っています
ここは要検討ですね

また、冒頭でも言いましたが、自分は初学者ですので、もっと簡単にできる等あれば、是非教えていただけるとありがたく感じます

ということで失礼します

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

rails_クリックでカウントアップとデータの保存

タイトルのように、クリックするごとに数字をカウントアップし、非同期でデータを保存する方法を紹介します

イメージとしては、動画を確認してください⬇️

自分もプログラミング初学者なので、もっとやり方あると思いますが、ググっても良い記事がなく、同じような実装に悩まれている方が多かったので、「とりあえず実装できる」レベルですが、誰かの参考になればと思います

あと、ここで例として紹介しているアプリケーションの内容は小説を定義(maintitle)し、その小説の内容(story)を随時アップしていくという内容になっていますので、なんとなくイメージしていただくと記事の内容が理解しやすいかなと思います

また、マイグレーションファイルの作成や、コントローラの作成など、ここで紹介していない部分の実装等については、別の方の記事等を参考にしてください

それではちょっと長くなりますが紹介していきます

  • ruby 2.5.1
  • rails 5.2.3

使用しているgem

  • 'gon'
  • 'device'
  • 'jquery-rails'
○○_create_reviews.rb
class 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
app/models/review.rb
class Review < ApplicationRecord
  belongs_to :user
  belongs_to :story
  belongs_to :maintitle
end
config/routes.rb
resources :maintitles do
    # 省略
    resources :stories do
      post 'like_review'
      delete 'unlike_review' #まだ実装できてません・・
      resources :comments, only: [:create]
    end
  end
app/controllers/stories.controller.rb
before_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
app/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.js
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);
};

$(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.rb
class 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.rb
class Review < ApplicationRecord
  belongs_to :user
  belongs_to :story
  belongs_to :maintitle
end

これはこのままでいいと思います
※ コードは載せませんが、ここでアソシエーションを組んでいる相手方には、has_many :reviewsを書いてください

routeを設定

config/routes.rb
resources :maintitles do
    # 省略
    resources :stories do
      post 'like_review'
      delete 'unlike_review' #まだ実装できてません・・
      resources :comments, only: [:create]
    end
  end

maintitlesにstoriesをネストさせ、storiesコントローラ内に

  • like_review
  • unlike_review

のアクションを設定します(後述)
ネストさせる意味は、reviewテーブルにmaintitle_idとstory_idを保存するためにやってます

controllerを設定

app/controllers/stories.controller.rb
before_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.js
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);
};

$(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アクション」するという方法にシフトチェンジしました
なので、評価していなくてもデータが作成されるため、本当にこの実装の仕方でいいのか疑問が残っています
ここは要検討ですね

また、冒頭でも言いましたが、自分は初学者ですので、もっと簡単にできる等あれば、是非教えていただけるとありがたく感じます

ということで失礼します

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

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 library

Book モデルを編集する

データベースの接続先を library に変更します。

app/models/book.rb
class 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 library

db: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

参考情報

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

投稿に対してのコメントの削除した際にエラー 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  
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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使ってるのかな...
まぁとりあえず復旧だ。

Dockerfile
RUN apk add --update --no-cache less
docker-compose build

エラー出なくなった。

Reference

docker alpineでpryデバッグ中に less -Rが立ち上がらない - Qiita

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

N+1問題を解決ゾロリ

【結論】includesを使う

データベースにアクセスする回数を減らそう

データを取ってくる際にSQLというものが発行されます。

アソシエーションを組んでいる場合、子要素のデータを取ってくる時もあるでしょう。
例えばこんな時

index.html.haml
- @tweets.each do |tweet|
  = tweet.text
  = tweet.user.nickname

tweetsテーブルとusersテーブルからデータを取ってきてます
この時SQLは通常(tweetsテーブル)+1回(usersテーブル)発行されてしまいます。

SQLが多いと処理が重くなるので、1回で関連するデータを一気に持ってきちゃおうぜ!って話です。

UserモデルとTweetモデルが以下のアソシエーションを組んでいるとします。

user.rb
has_many :tweets
tweet.rb
belongs_to :user


【tweetsコントローラーを編集】

includes (:モデル名)
これで一気に関連データまで取ってくることができます。

tweets_controller.rb
def index
  @tweets = Tweet.includes(:user)  #allは省略可
end

これでN+1問題はバッチリ解決です!



ではまた!

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

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などを添えて。



超参考

https://railsguides.jp/active_storage_overview.html



ではまた!

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

before_action

いつ使うの

before_actionを使えば、すべてのアクションのが実行される前に指定したメソッドを呼び、共通の処理を実行することができます。

使い方

before_action :メソッド名

これをコントローラーの一番上の書きます。



オプションとしてonlyとexceptがあります。

only: [:アクションA]
→アクションAのときのみ

except: [:アクションB]
→アクションB以外のとき

tweets_controller.rb
class 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
end

indexアクション以外のとき、まず初めにmove_to_indexを実行します。

move_to_indexはログインしていない場合に、indexアクションへ遷移させるというものです。
これで、ログインしていないと新規投稿などができなくなります。





ちなみにdeviseがインストールされている場合は、
authenticate_user!というヘルパーメソッドを使って省略できます。

tweets_controller.rb
class TweetsController < ApplicationController
  before_action :authenticate_user!, except: [:index]

  def index
  end

  def new
  end

  中略

end



deviseのヘルパーメソッドはこちらへ

https://qiita.com/drafts/5c3c4abd35af82b3e7c3



ではまた!

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

【Rails】統合テストによるリンクのテスト【Rails Tutorial 5章まとめ】

link_toと名前付きルートで作ったリンクが正常に表示され、機能しているかどうかを統合テストで確認する。
統合テストはアプリケーションの動作をシミュレートするために使用される。

$ rails generate integration_test site_layout

テストの手順

①ルートURL (Homeページ) にGETリクエストを送る
②正しいページテンプレートが描画されているかどうか確かめる
③Home、Help、About、Contactの各ページへのリンクが正しく動くか確かめる

test/integration/site_layout_test.rb
  test "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
  end
assert_select "a[href=?]", about_path

はlink_toによって生成されたaタグを調べている。
?の部分に第二引数のパスが代入される。
countオプションによってリンクの個数も調べることができる。

assert_selectについては別記事にまとめたいと思っているが、分かりやすいサイトを見つけたのでここにメモしておく。
assert_selectの使い方
https://zariganitosh.hatenablog.jp/entry/20080405/1207455670

これ以降、レイアウトに新規のアクションとそのリンクを作成した場合は、このテストに書いていくことになる。

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

【Rails】ルーティングと名前付きルート【Rails Tutorial 5章まとめ】

ルートURL

rootを使い、ルートURLを指定する。

config/routes.rb
root 'static_pages#home'

ルートURLは、root_pathまたはroot_urlで呼び出せる(名前付きルート)。

root_path -> '/'
root_url  -> 'http://www.example.com/'

前者はルートURL以下の文字を、後者はURLの全文を表示する。
基本的には前者を使い、後者はリダイレクトの場合に使用する。

そのほかのルーティング

config/routes.rb
get '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.rb
 test "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.rb
get  '/help', to: 'static_pages#help', as: 'helf'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

resourcesメソッドについて

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

【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で呼び出す場合は、アンダーバーはつけない。

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

devise で認証キーを email から username に変更する

はじめに

手元に同志が作った Rails アプリがありましたとさ。
これを流用・シュリンクして別の目的のアプリに改変することにしましたとさ。
認証には devise が利用されていましたとさ。定期。

なんか手元で試したら、ネットで見かける改変手順より全然少なかったので、ちょっと整理しておこうかと思った次第です。

結論

核としては、モデルにフィールドを追加して config/initializers/devise.rbconfig.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 the

devise には Strong Parameters のデフォルトがある

ここを見ると幾つかの予約語が定義されています。password とか remember_me とか。
そしてここauthentication_keys を参照しています。

これらはユーザーが能動的に指定しなくても Strong Parameters として処理されます。

ところで

この変更をする前に作ったユーザーは、変更後、仮に username を追加できたとしても認証できないです。ご注意を。

おわりに

devise も黒魔術ですね。すごいなぁ。

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

【Rails】bootstrap導入【Rails Tutorial 5章まとめ】

Gemfileにbootstrapを記述。

Gemfile.rb
gem 'bootstrap-sass', '3.3.7'

$ bundle installしておく。
レイアウト用のcssを作って@importを使ってbootstrapを読み込む。

app/assets/stylesheets/custom.scss
@import "bootstrap-sprockets";
@import "bootstrap";
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む