20200704のRailsに関する記事は22件です。

【Rails】form_withの属性や基本構文

はじめに

チーム開発にて Ruby on Rails を用いてWebアプリを作成中です。

まだまだ未熟なため、記述やコードがわかりにくいかもしれません。
記載しきれなかったものや変更点は、随時更新・ブラッシュアップしていきます。

私と同じ初学者の方は検索に検索を重ねると「いま自分が何を調べたいのか」が分からなくなってしまいがちですので、専門用語やカタカナ用語は都度 解説します。

form_with
フォーム実装のためのヘルパーメソッド。
Rails5.1よりform_withが導入され、form_forやform_tagが非推奨となり今後 削除される予定となっている。一つの構文でフォームが書けるようになった。
HTTPメソッドを指定する必要が無い。form_withではinputタグは用いない。

html.haml
.form-wrapper
  = form_with url: credit_cards_path, method: :post, model: @card, local: true, html:{name: "inputForm"}, class: 'form-wrapper', id: 'charge-form' do |form|

解説します。

= form_with 〜 do |form|
-# 例えるなら、お菓子作りでいうケーキカップ(= form_with)と袋と紐(do |form|)
url: credit_cards_path
-# パスは関連モデルがある場合、省略可。
method: :post
-# formのデフォルトHTTPメソッドはPOST、省略可。
model: @card
-# ActiveRecordを継承するモデルのインスタンスが利用可。
local: true
-# デフォルトで非同期通信リクエストで送信されるようになる。remote: trueの指定をする必要は無い。

非同期通信
コンピュータ間で送信者のデータ送信タイミングと受信者のデータ受信タイミングを合わせずに通信を行う通信方式。つまり、送信者と受信者の両方がオンラインである必要がなく、片方が接続しているだけで通信が成立する。

html:{name: "inputForm"}
-# name: "inputForm"(html:{}が無い)だとエラーになる。
class: 'form-wrapper', id: 'charge-form'
-# class属性やid属性にはHTML属性は不要。

form_with ヘルパー表

オプション 説明 デフォルト値
:url フォームに入力されたデータを送信するURL。名前付きルートを直接使用可能 nil
:method HTTPメソッドの指定。 POST
:format 送信するデータ形式 JSON形式やXML形式など:urlオプションが指定された場合は無視される。 text/html
:scope ルーティングで名前空間が指定されている場合に利用するPrefix指定。 nil
:model モデルオブジェクト。オブジェクトが新規レコードの場合は作成フォームが生成され、既存レコードの場合は更新フォームが生成される。 nil
:authenticity_token 認証トークンの指定。カスタム認証トークンでオーバーライドするかfalse、認証トークンフィールドをスキップする。
:local リモートフォームを使わない設定。 false
:skip_enforcing_utf8 IE5以前の文字化け対策(UTF-8の矯正送信)のスキップ設定。 false
:builder フォームオブジェクトのオーバーライド(オリジナルフォームコントロールの作成) nil
:id オプションのHTML id属性 nil
:class オプションのHTML class属性 nil
:data オプションのHTML data属性 nil
:html id、class、data以外のオプションHTML属性 nil

おわりに

form_withのまとめは以上となります。
例がお役に立てば幸いです。

参考記事

form_for/form_tag/form_withについて【概要+使い方】
form_withまとめ

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

モデル作成、削除等のあれこれ

毎回忘れるので、自分の忘備録用に

モデルの作成

外部キー制約(※1)をつけるならreferrence型で作成(インデックスは自動で付貼ってくれる)
カラムはuser_idではない点に注意

rails g model Article title:string text:text user:references

(*1)存在しない値の外部キーは登録できない(整合性がとれる)、親テーブルに外部キーが登録されている子テーブルのリソースは削除できない


外部キー制約をつけないならintger型で作成(インデックスを貼るならマイグレーションファイルで記述が必要)
カラムはuser_idにする

rails g model Article title:string text:text user_id:intger


単純にマイグレーションファイルだけの作成

rails g migration Artcle

モデルの型

  • string : 文字列(255字まで)
  • text : 文字列(string以上の文字数の場合)
  • references: 外部キー
  • integer : 整数
  • float : 浮動小数点
  • datetime : 日時
  • time : 時間
  • date : 日付
  • boolean : trueまたはfalse

削除

マイグレーションを実行する前か後かで操作が変わるので注意(rails db:migrateをしたかしてないか)

rails db:migrate前ならdestroyで削除

rails destroy model Article

rails db:migrate後ならロールバックで戻す

rails db:rollback #1つ前
rails db:rollback STEP=2 #2つ前
rails db rollback VERSION=202007012345 #バージョン指定で戻す

そのあとに rails destroy model Article で完了。

カラムの追加と削除

カラムの追加

rails g migration AddSubtitleToArticles subtitle:string



カラムの削除

rails g migration RemoveSubtitleFromArticles subtitle:string

テーブル名Artcles(複数形)になる点に注意
マイグレーションファイルではchangeメソッドの中に記述される。

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

Ruby on Rails チュートリアル(第4版) 第6章

6.1.1 演習

1.Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。これはデータベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われます。さて、あなたの環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル (リスト 6.2) の内容を比べてみてください。
 省略

2.ほぼすべてのマイグレーションは、元に戻すことが可能です (少なくとも本チュートリアルにおいてはすべてのマイグレーションを元に戻すことができます)。元に戻すことを「ロールバック (rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。
 省略

3.もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください。
 省略

6.1.2 演習

1.Railsコンソールを開き、User.newでUserクラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください (ヒント: 4.4.4で紹介したテクニックを使ってみてください)。

>> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> user.class
=> User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime)
>> user.class.superclass
=> ApplicationRecord(abstract)

2.同様にして、ApplicationRecordがActiveRecord::Baseを継承していることについて確認してみてください。

>> user.class.superclass.superclass
=> ActiveRecord::Base

6.1.3 演習

1.user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。

>> user.name.class
=> String
>> user.email.class
=> String

2.created_atとupdated_atは、どのクラスのインスタンスでしょうか?

>> user.created_at.class
=> ActiveSupport::TimeWithZone
>> user.updated_at.class
=> ActiveSupport::TimeWithZone

6.1.4 演習

1.nameを使ってユーザーオブジェクトを検索してみてください。また、 find_by_nameメソッドが使えることも確認してみてください (古いRailsアプリケーションでは、古いタイプのfind_byをよく見かけることでしょう)。

>> User.find_by(name: "Michael Hartl")
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2020-07-04 11:43:14", updated_at: "2020-07-04 11:43:14">
>> User.find_by_name("Michael Hartl")
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2020-07-04 11:43:14", updated_at: "2020-07-04 11:43:14">

2.実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。

>> User.all.class
=> User::ActiveRecord_Relation

3.User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質があります。これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」。(訳注: そういえばRubyKaigi 2016の基調講演で、Ruby作者のMatzがダックタイピングについて説明していました。2〜3分の短くて分かりやすい説明なので、ぜひ視聴してみてください!)

>> User.all.length
  User Load (0.2ms)  SELECT "users".* FROM "users"
=> 2

6.1.5 演習

1.userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。

>> user.name = "Muramako"
=> "Muramako"
>> user.save
   (0.1ms)  SAVEPOINT active_record_1
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

2.今度はupdate_attributesを使って、email属性を更新および保存してみてください。
 特定の属性のみを更新したい場合は、次のようにupdate_attributeを使います。の文があるので、こちらを使います。

>> user.update_attribute(:email, "Muramako@example.com")
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.1ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["email", "Muramako@example.com"], ["updated_at", "2020-07-04 11:59:33.683197"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

3.同様にして、マジックカラムであるcreated_atも直接更新できることを確認してみてください。ヒント: 更新するときは「1.year.ago」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。

>> user.update_attribute(:created_at, 1.year.ago)
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.1ms)  UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["created_at", "2019-07-04 12:00:26.106554"], ["updated_at", "2020-07-04 12:00:26.107328"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

6.2.1 演習

1.コンソールから、新しく生成したuserオブジェクトが有効 (valid) であることを確認してみましょう。

>> User.new.valid?
=> true

2.6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。

>> user.valid?
Traceback (most recent call last):
        1: from (irb):2
NameError (undefined local variable or method `user' for main:Object)
Did you mean?  super

6.2.2 演習

1.新しいユーザーuを作成し、作成した時点では有効ではない (invalid) ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。

>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.invalid?
=> true
>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]

2.u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?
 自力で分からず。無念。
 u.email.errors.messages u.errors.messages(:email)等試したけどダメ。
 ググった結果正解はu.errors.messages[:email]惜しい!
 find_byの時なんかは、()だと思うんだけど、エラー調べるときは[]なのね(何でだろう)。

6.2.3 演習

1.長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。

>> user = User.new(name: "Muramakooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo", email: "Muramakooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo@example.com")
=> #<User id: nil, name: "Muramakooooooooooooooooooooooooooooooooooooooooooo...", email: "Muramakooooooooooooooooooooooooooooooooooooooooooo...", created_at: nil, updated_at: nil>
>> user.invalid?
=> true

2.長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。

>> user.errors.messages=> {:name=>["is too long (maximum is 50 characters)"], :email=>["is too long (maximum is 255 characters)"]}

6.2.4 演習

1.リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
 省略
2.先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。

  test "email validation should reject invalid addresses" do
    invalid_addresses = %w[foo@bar..com 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

 REDになった。リスト6.23を反映したらGREENになった。

3.foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
 省略

6.2.5 演習

1.リスト 6.33のように、メールアドレスを小文字にするテストをリスト 6.26に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして redになることを、また、コメントアウトを解除すると greenになることを確認してみましょう。
省略

2.テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。
 省略

6.3.2 演習

1.この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。

>> user = User.new(name: "Muramako", email: "Muramako@example.com")
=> #<User id: nil, name: "Muramako", email: "Muramako@example.com", created_at: nil, updated_at: nil, password_digest: nil>
>> user.valid?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "Muramako@example.com"], ["LIMIT", 1]]
=> false

2.なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。

>> user.errors.messages
=> {:password=>["can't be blank"]}

6.3.3 演習

1.有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。

>> user = User.new(name: "Muramako", email: "Muramako@example.com", password: "mmm")
=> #<User id: nil, name: "Muramako", email: "Muramako@example.com", created_at: nil, updated_at: nil, password_digest: "$2a$10$/pcBPDq8e44IQ/8vLxMR3e3xVyMOOk.G35K.yCFFEWO...">
>> user.invalid?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "Muramako@example.com"], ["LIMIT", 1]]
=> true

2.上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。

>> user.errors.messages
=> {:password=>["is too short (minimum is 6 characters)"]}

6.3.4 演習

1.コンソールを一度再起動して (userオブジェクトを消去して)、このセクションで作ったuserオブジェクトを検索してみてください。

user = User.find_by(id:1)

2.オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?

>> user.email = "mhartl@example.jp"
=> "mhartl@example.jp"
>> user.save
   (0.1ms)  begin transaction
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ?  [["email", "mhartl@example.jp"], ["id", 1], ["LIMIT", 1]]
   (0.0ms)  rollback transaction
=> false
>> user.errors.messages
=> {:password=>["can't be blank", "is too short (minimum is 6 characters)"]}

3.今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。

>> user.update_attribute(:email, "mhartl@example.jp")
   (0.1ms)  begin transaction
  SQL (2.0ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["email", "mhartl@example.jp"], ["updated_at", "2020-07-04 13:29:40.208038"], ["id", 1]]
   (6.7ms)  commit transaction
=> true

謎のエラー

$ rails test (GREEN)
$ git add -A
$ git commit -m "Make a basic User model (including secure passwords)"
$ git checkout master
$ git merge modeling-users
$ git push
$ rails test

ここでRED。さっきはGREENだったのに、なぜ?
You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle instal
Gemfileに、gem 'bcrypt', '3.1.12'はあるのに。
bundle installrails tでもやっぱりRED。
ググってみたら、一度Gemfileのgem 'bcrypt', '3.1.12'をコメントアウト→bundle install→コメント解除→bundle install
これでGREENに。なぜなのかは分からないけれど。
無事GREENになったのでherokuへpush!

ここでまたエラー!

$ heroku run rails db:migrate
bash: heroku: command not found

ググった。
heroku command not found の対処に載っているコードを入力。
うまくいきました。ありがたや。

メモ

  • コントローラ名には複数形を使い、モデル名には単数形を用いるという慣習がある。(例)コントローラはUsersでモデルはUser。
  • dupは、同じ属性を持つデータを複製するためのメソッド。
  • case_sensitive: falseは大文字小文字を区別するかどうかを指定するオプション。
  • マイグレーション名にto_usersをつけると、usersテーブルにカラムを追加するマイグレーションがRailsによって自動的に作成される。
  • has_secure_passwordを追加すると、そのオブジェクト内でauthenticateメソッドが使えるようになる。このメソッドは、引数に渡された文字列 (パスワード) をハッシュ化した値と、データベース内にあるpassword_digestカラムの値を比較する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]部分テンプレートの使い方

部分テンプレートとは

複数のビューファイルの中で使われている部分を一つのビューファイルとして管理する時に使います。

部分テンプレート用ビューファイルの作り方

部分テンプレートとして呼び出す場合は、ファイル名の頭に_(アンダーバー)を作成します。
new.html.erbのファイルを呼び出したい場合は、_new.html.erbになるということです。

部分テンプレートの呼び出し方

部分テンプレートの呼び出しには以下のようにrenderメソッドを使用します。

render 'ファイル名'

ただし、同じディレクトリでない場合の呼び出しは以下のようにフォルダ名の記述も必要です。

render 'フォルダ名/ファイル名'

まとめ

今回が**部分テンプレートの使い方を簡潔にまとめました。
個人開発アプリも日々進んでいるので近いうちに実際に使用した機能のアウトプットも行います。

よかったら参考にして下さい!

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

【Docker】初心者が環境構築しました!

背景

未経験から転職した僕が初めてDockerの環境構築をしたので、アウトプットします。併せて、同じ初心者の方の参考になれば嬉しいです。

使用環境

  • Macを使用しています
  • 開発フレームワークはRuby on Railsです

Dockerとは

仮想環境を提供するオープンソースソフトウェアです。
仮想環境というのは、自分のPCやサーバーとは別の環境のことをいいます。
このメリットは何のか?といいますと例を2つくらい挙げたいと思います。

  1. MacPCを使用しいてWindows専用のソフトを使いたい!という時に仮想環境を作ってWindowsの環境を作り出せばそこでWindowsのソフトを作れちゃうって感じになります!

  2. 自分のPCのプログラミング言語のバージョンが5だとして、仕事の現場のバージョンが3を使っていたとしたらわざわざ自分のPCのバージョンを落さなくてもこの仮想環境情で開発できちゃうって訳です!!

これは便利ですね?(初心者ながら思っています、、笑)

なぜDockerを使うのか

理由は主に2点あります。
1. 環境構築が簡単
既に開発が行われている現場な難しい環境構築をしなくても簡単なコマンド(docker compose)を打てばすぐに開発に移れます。
2. 移動のしやすさ
イメージで言うとdockerと言う船にコンテナと言うパーツ(プログラミング言語Webサーバー、データベース等)が乗っている感じです。これはコマンドで簡単に移動できるみたいです。(まだ体験していないですが、、)

環境構築手順

※以下はまだDockerとDocker Composeを自分のPCにインストールしていない方向けです。(初めてDockerを使う方)
1. まずは下記のURLでDockerのインストールから始めましょう!
https://www.docker.com/
2. インストールが終わったらターミナルを開いて下記の作業をしてください。
そして、% docker --versionと% docker-compose --version(%は除いてください) と打ってください。
下記の様に出ていればインストール成功です。
Screenshot from Gyazo

まとめ

今回は概念の理解と簡単な環境構築手順をまとめてみました。
勉強し始めで間違っているところあるかもしれませんが、Dockerってすごく便利だ
なって思いました。今後も勉強してまた記事を更新していきたいと思います!!

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

has_secure_passwordとpresenceとallow_nilと私。

Railsチュートリアル(第6版)の10.1.4 -TDDで編集を成功させる でつまり、
午前中いっぱいかけて解消させた疑問の備忘録です。
私以外の人はこんなところでつまらないかもしれないですが・・

つまったところと疑問点

つまったところ:パスワードが空のままでも更新できるようにする

「ユーザー情報を編集する際、いちいちパスワードを入力するのは不便なので
パスワードを変えないときは、入力しなくてもユーザー情報を更新できるようにする。」
という内容。

具体的には、現状

user.rb
class User < ApplicationRecord
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

となっていて、nilや空文字、6文字未満のパスワードは受け入れられないところを

user.rb
class User < ApplicationRecord
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

と、allow_nilというオプションで、パスワードが空だった時の例外処理を与える(バリデーションをスキップする)よ。

ということでした。

何が疑問なの?という方、どうかこの記事をそっと温かくスキップしてください:see_no_evil:

疑問点:そもそも、presence: true書く必要あったの??

なぜにそんな疑問が生まれてしまったのかというと、チュートリアル中の、
「allow_nilによって新規登録時にパスワードがなくても登録できちゃう!?と心配になるけど、
has_secure_passwordがオブジェクト生成時に存在性を検証するようになっているから、
その心配もいらないよ!」
という文。
(簡略化してます。語弊があったら申し訳ないです。。)

以下、私の疑問と疑問が生まれる原因となった勘違いです。

  • 新規登録時はパスワード必須にしたい → has_secure_passwordで検証できる
  • 編集時はパスワード入れなくても更新できるようにしたい → has_secure_passwordはオブジェクト生成時にのみ、存在性を検証するので、編集時はパスワード入れなくてもOK! ⇒ あれ、has_secure_passwordだけで完結する?presence: trueいらなくない・・?

大事なことを見落としていたわけですね。

結論:編集時、空の場合はスキップしたいが、空文字の場合はバリデーションかけねば。

改めて、下記3点を明確にしておきたいと思います。

  • has_secure_password:DBにレコードが生成された時だけ存在性のvalidationをおこなう
  • presence: true : 値がnil空文字でないことを確認
  • allow_nil : 対象の値がnilの場合にvalidationをスキップ = 値が空文字の場合はvalidationにひっかかる

つまり、presence: trueは書いておかないと、
「編集時、パスワード欄にスペースを打ちこまれちゃった場合にも更新ができてしまう」
のでしたね・・

どうやって疑問を解消したのか

presence: trueいらなくない・・?」と思っていたので
presence: trueを消して、下記の状態でテストを回してみました。

user.rb
class User < ApplicationRecord
  has_secure_password
  validates :password, length: { minimum: 6 }, allow_nil:true

すると下記統合テストはGREENに。

test/integration/users_edit_test.rb
  test "successful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    name = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: {name: name,
                                            email: email,
                                            password: "",
                                            password_confirmation: "" }}
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name, @user.name
    assert_equal email, @user.email
  end

やはりpresence: trueいらないのか・・?と思ったそのとき、
別のテストが私に思い出させてくれました。

user_test.rb
  test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

こちらがしっかりREDになっていた!

まずこのテストはユーザを新規登録せずにパスワードの値を空文字に設定しているため
has_secure_passwordの検証にはひっかかりません。

そしてpresence: trueを消してしまったがために、空文字入力がOKになり、
ユーザーが有効になってしまったのでテストがREDになっていたのでした。

ここでしっかり、" " * 6空文字がテストされていたおかげで
自分の勘違いに気づくことができました。。

同じようにうっかりここで詰まってしまった方のご参考になれば幸いです。

参考

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

Ruby on Rails チュートリアル(第4版) 第5章

5.1.1 演習

1.Webページと言ったらネコ画像、というぐらいにはWebにはネコ画像が溢れていますよね。リスト 5.4のコマンドを使って、図 5.3のネコ画像をダウンロードしてきましょう8 。

$ curl -OL cdn.learnenough.com/kitten.jpg

2.mvコマンドを使って、ダウンロードしたkitten.jpgファイルを適切なアセットディレクトリに移動してください (参考: 5.2.1)。

$ mv kitten.jpg app/assets/images/

3.image_tagを使って、kitten.jpg画像を表示してみてください (図 5.4)。

/app/views/static_pages/home.html.erb
<%= image_tag("kitten.jpg", alt: "kitten") %>

5.1.2 演習

1.リスト 5.10を参考にして、5.1.1.1で使ったネコ画像をコメントアウトしてみてください。また、ブラウザのHTMLインスペクタ機能を使って、コメントアウトするとHTMLのソースからも消えていることを確認してみてください。

/app/views/static_pages/home.html.erb
<%#= image_tag("kitten.jpg", alt: "kitten") %>

2.リスト 5.11のコードをcustom.scssに追加し、すべての画像を非表示にしてみてください。うまくいけば、Railsのロゴ画像がHomeページから消えるはずです。先ほどと同様にインスペクタ機能を使って、今度はHTMLのソースコードは残ったままで、画像だけが表示されなくなっていることを確認してみてください。
 省略

5.1.3 演習

1.Railsがデフォルトで生成するheadタグの部分を、リスト 5.18のようにrenderに置き換えてみてください。ヒント: 単純に削除してしまうと後でパーシャルを1から書き直す必要が出てくるので、削除する前にどこかに退避しておきましょう。
 省略
2.リスト 5.18のようなパーシャルはまだ作っていないので、現時点ではテストは redになっているはずです。実際にテストを実行して確認してみましょう。
 RED
3.layoutsディレクトリにheadタグ用のパーシャルを作成し、先ほど退避しておいたコードを書き込み、最後にテストが green に戻ることを確認しましょう。

 パーシャルを作成

$ touch app/views/layouts/_rails_default.html.erb
/sample_app/app/views/layouts/_rails_default.html.erb
<%= csrf_meta_tags %>
<%= stylesheet_link_tag    'application', media: 'all',
                           'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application',
                           'data-turbolinks-track': 'reload' %>

 GREENに・・・ならず。
 /home/ec2-user/environment/sample_app/db/schema.rb doesn't exist yet. Run rails db:migrate to create it, then try again. If you do not intend to use a database, you should instead alter /home/ec2-user/environment/sample_app/config/application.rb to limit the frameworks that will be loaded.
 とのことなので、
 rails db:migrateを実行
 まだエラーが出たものの、コードに間違いは無さそうなのでとりあえずサーバーを再起動 Ctrl+Crails s
 GREENになった。良かった!

5.2.2 演習

1.5.2.2で提案したように、footerのCSSを手作業で変換してみましょう。具体的には、リスト 5.17の内容を1つずつ変換していき、リスト 5.20のようにしてみてください。
 省略

5.3.2 演習

1.実は名前付きルートは、as:オプションを使って変更することができます。有名なFar Sideの漫画に倣って、Helpページの名前付きルートをhelfに変更してみてください (リスト 5.29)。
 省略

2.先ほどの変更により、テストが redになっていることを確認してください。リスト 5.28を参考にルーティングを更新して、テストを greenにして見てください。

/test/controllers/static_pages_controller_test.rb
  test "should get help" do
    get helf_path
    assert_response :success
    assert_select "title", "Help | #{@base_title}"
  end

3.エディタのUndo機能を使って、今回の演習で行った変更を元に戻して見てください。
 Ctrl+Zのことらしい。元に戻して保存。

5.3.3 演習

1.リスト 5.29のようにhelfルーティングを作成し、レイアウトのリンクを更新してみてください。

/config/routes.rb
  get  '/help',    to: 'static_pages#help', as: 'helf'
/app/views/layouts/_header.html.erb
<li><%= link_to "Help",   helf_path %></li>

2.前回の演習と同様に、エディタのUndo機能を使ってこの演習で行った変更を元に戻してみてください。
 省略

5.3.4 演習

1.footerパーシャルのabout_pathをcontact_pathに変更してみて、テストが正しくエラーを捕まえてくれるかどうか確認してみてください。

/app/views/layouts/_footer.html.erb
<li><%= link_to "About",   contact_path %></li>

 REDになる。Expected at least 1 element matching "a[href="/about"]", found 0..

2.リスト 5.35で示すように、Applicationヘルパーで使っているfull_titleヘルパーを、test環境でも使えるようにすると便利です。こうしておくと、リスト 5.36のようなコードを使って、正しいタイトルをテストすることができます。ただし、これは完璧なテストではありません。例えばベースタイトルに「Ruby on Rails Tutoial」といった誤字があったとしても、このテストでは発見することができないでしょう。この問題を解決するためには、full_titleヘルパーに対するテストを書く必要があります。そこで、Applicationヘルパーをテストするファイルを作成し、リスト 5.37のFILL_INの部分を適切なコードに置き換えてみてください。ヒント: リスト 5.37ではassert_equal <期待される値>, <実際の値>といった形で使っていましたが、内部では==演算子で期待される値と実際の値を比較し、正しいかどうかのテストをしています。

/test/helpers/application_helper_test.rb
require 'test_helper'

class ApplicationHelperTest < ActionView::TestCase
  test "full title helper" do
    assert_equal full_title,         "Ruby on Rails Tutorial Sample App"
    assert_equal full_title("Help"), "Help | Ruby on Rails Tutorial Sample App"
  end
end

5.4.1 演習

1.表 5.1を参考にしながらリスト 5.41を変更し、users_new_urlではなくsignup_pathを使えるようにしてみてください。

/test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  test "should get new" do
    get signup_path
    assert_response :success
  end

end

2.先ほどの変更を加えたことにより、テストが redになったことを確認してください。なお、この演習はテスト駆動開発 (コラム 3.3) で説明した red/green のリズムを作ることを目的としています。このテストは次の5.4.2で greenになるよう修正します。
 REDになる。NameError: undefined local variable or method `signup_path' for #UsersControllerTest:0x00000000044804f0

5.4.2 演習

1.もしまだ5.4.1.1の演習に取り掛かっていなければ、まずはリスト 5.41のように変更し、名前付きルートsignup_pathを使えるようにしてください。また、リスト 5.43で名前付きルートが使えるようになったので、現時点でテストが greenになっていることを確認してください。
 GREENになった。

2.先ほどのテストが正しく動いていることを確認するため、signupルートの部分をコメントアウトし、テスト redになることを確認してください。確認できたら、コメントアウトを解除して greenの状態に戻してください。

/config/routes.rb
  # get  '/signup',  to: 'users#new'

 REDになった。NameError: undefined local variable or method `signup_path' for #UsersControllerTest:0x0000000002acb078
 元に戻したらGREENになった。

3.リスト 5.32の統合テストにsignupページにアクセスするコードを追加してください (getメソッドを使います)。コードを追加したら実際にテストを実行し、結果が正しいことを確認してください。ヒント: リスト 5.36で紹介したfull_titleヘルパーを使ってみてください。

/test/integration/site_layout_test.rb
get signup_path
    assert_select "title", full_title("Sign up")

メモ

assert_equal <期待される値>, <実際の値>は、内部では==演算子で期待される値と実際の値を比較し、正しいかどうかのテストをしている。

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

create時だけ値を生成してupdate時は変更できないようにしたい

numberというカラムを作成。numberはとある条件を元に数字を計算し、それが値として入る。(今回の記事に関係ないので具体的なコードや条件は割愛)
新規登録時にはbefore_saveを経由して上記条件に基づきカラムの値を生成するが、更新時にもその値が変わらないようにしたい場合。

before_saveに書いたメソッドは登録更新どちらのsave時にも経由してしまうため、updateのときだけ通らないようにしたらいいのでは?と考えて以下の実装。

attr_accessorを使用。
参考にさせていただいた記事
https://kei178.me/programming/1416/

Hoge.rb
  attr_accessor :number_required
  before_save :apply_number, if: :number_required?

  def number_required?
    number_required
  end

  def apply_number
    self.number = number_for_new_post
    true
  end

  def number_for_new_post
   # 適切な数字を生成するための処理
  end
controller
  def create
    @hoge  = Hoge.new(create_hoge_params)
    @hoge.number_required = true    # この一行追加
    if @hoge.save
      redirect_to root_path
    else
      render :new
    end
  end
...

  def update
    begin
      @hoge = Hoge.find(params[:id])
      @hoge.number_required = false # ここの一行追加
      @hoge.update!(update_hoge_params)
      redirect_to hoge_path
    rescue ActiveRecord::RecordNotFound => e
      flash[:alert] = "更新失敗しました"
      redirect_to hoge_path
    rescue ActiveRecord::RecordInvalid => e
      render :edit
    end
  end
...

 private
  def create_hoge_params
    # 新規作成時に必要なカラム
  end

  def update_hoge_params
    # 更新時に必要なカラム
  end

これだと確かにupdateのときはbefore_saveを通らない。
しかし今回のように新規登録時にはnumberが必ず必要となる場合、number_required?だとわかりづらい。。

そこで使うのがnilガード

そもそもbefore_saveを経由しても値が変わらないようにすれば問題がなかったため、以下のように修正するだけでよかった。

参考にさせていただいた記事
https://note.com/vixer93/n/n20c409919665

Hoge.rb
  attr_accessor :number_required
  before_save :apply_number

  def apply_number
    self.number ||= number_for_new_post
    true
  end

numberに値がなければnumber_for_new_postメソッドの返り値がnumberに。
もしnumberがもう入ってればそのまま。

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

Rails5でECサイトを作る② ~Bootstrap4設定、Controller・アクション定義~

はじめに

架空のベーカリーで買い物できるECサイトを作るシリーズ、Rails5でECサイトを作る①の続きです。
必要なControllerとViewを揃え、アクションも定義だけ書いて、最低限のページ遷移ができるようにします。

ソースコード

https://github.com/Sn16799/bakeryFUMIZUKI

Controller

コントローラを一気に作ります。Viewも伴うアクションは、ここでまとめて作ってしまいます。

Customerサイト

$ rails g controller addresses edit index
$ rails g controller cart_items index
$ rails g controller customers edit show withdraw
$ rails g controller genres index show
$ rails g controller homes about top
$ rails g controller order_items index
$ rails g controller orders confirm index new show thanks
$ rails g controller products index show

Adminサイト

$ rails g controller admin::customers edit index show
$ rails g controller admin::genres edit index
$ rails g controller admin::homes top
$ rails g controller admin::order_items
$ rails g controller admin::orders index show
$ rails g controller admin::products edit index new show
$ rails g controller admin::searches search

アクション

ViewのないアクションをControllerファイルに記述していきます。
詳しい内容は後で書くので、今は一通りアクションを定義して、ページ遷移できることを優先します。

Customerサイト

app/controllers/addresses_controller.rb
  def create
  end

  def update
  end

  def destroy
  end
app/controllers/cart_items_controller.rb
  def create
  end

  def update
  end

  def destroy
  end

  def destroy_all
  end
app/controllers/customers_controller.rb
  def udpate
  end

  def withdraw_done
  end
app/controllers/order_items_controller.rb
  def new
  end
app/controllers/orders_controller.rb
  def create
  end

Adminサイト

app/controllers/admin/customers_controller.rb
  def update
  end
app/controllers/admin/genres_controller.rb
  def create
  end

  def update
  end
app/controllers/admin/order_items_controller.rb
  def update
  end
app/controllers/admin/orders_controller.rb
  def update
  end
app/controllers/admin/products_controller.rb
  def create
  end

  def update
  end

Bootstrap4 読み込み

gem(bootstrap, jquery-rails)のインストールについては前回行ったので割愛します。

app/assets/stylesheets/application.cssapplication.scssにリネームしておきます。
そのファイル内にBootstrapの読み込みを記述します。

appseets/stylesheets/application.scss
@import 'bootstrap';

javascriptファイルにも同様に記述を行います。

app/assets/javascripts/application.js
//= require rails-ujs
//= require activestorage
--------------------------------
//= require jquery3
//= require popper                #ここを追記
//= require bootstrap-sprockets
--------------------------------
//= require_tree .

これでBootstrapを利用できるようになりました。試しに適当なところで使ってみましょう。

app/views/homes/top.html.erb
<h1>Homes#top</h1>
<p>Find me in app/views/homes/top.html.erb</p>

<%= link_to 'ABOUT', homes_about_path, class: 'btn btn-info' %>

localhost:3000/homes/top
homes_top.jpg

正しくページが表示され、"ABOUT"と書かれたリンクがボタンになりました! きちんとBootstrapが適用されていますね。
containerなどの枠組みをまだ何も設定していないので、全体的におそろしく端に寄ってしまいました。後ほど直しますが、今はとりあえず気にしないことにします。

後記

ひとまずRoutingとControllerを作ったので、必要なページは全て表示できるようになりました。次の記事から、具体的な機能の実装に入れそうです。
ところで、ページのデザインをどんな感じにするか、まだ迷っています。基本的にはBootstrapを下地としたレスポンシブ対応の設計にする予定ですが、おしゃれなCSSに挑戦するのも悪くないなぁと思ったり……。
今更ながら「パン屋の温かみあるイメージ」と「スタイリッシュな今風のサイト」が全然結び付かず、テーマ選びを間違えた感じがしているのですが、写真素材がすでに手元にあるのでこのまま進めます。
果たして、webサイトを違和感なく仕上げられるのか? 次回へ続く!

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

【Rails】初めてのDB設計

SQL(Structured Query Language)とは

  • リレーショナルDB(RDB)に命令を行う言語。
  • ISO(国際標準化機構)で規格化されており、どんなリレーショナルDBにも、同じ文法で操作する。
  • リレーショナルDB(RDB): Webアプリで一般的に使用される、表形式でデータ管理するDBのこと。(例:MySQL、PostgreSQL、SQliteなど)
  • 命令は2つに分類。
    定義: DDL(Data Definition Language)
    操作: DML(Data Manipulation Language)

【DDLの命令】

命令 機能
CREATE DB、テーブル作成
ALTER DB、テーブル更新
DROP DB、テーブル削除

【DMLの命令】

命令 機能
INSERT データ登録
UPDATE データ更新
DELETE データ削除
SELECT データ検索

ターミナルでのSQL文実行

  • SQLの実行には、DBへの接続が必要なので、以下のコマンドでmysqlrootユーザーで接続すると、MySQLが起動し、SQL文でMySQLが操作できる。
ターミナルでのSQL操作
-- mysqlに接続
 % mysql -u root

-- 今まで作ったDBの一覧表示
 mysql> SHOW DATABASES;

-- DB作成
 mysql> CREATE DATABASE DB;

-- DBを選択
 mysql> USE DB;

-- DB内のテーブル一覧表示
 mysql> SHOW TABLES;

-- テーブル作成
 mysql> CREATE TABLE テーブル名 (カラム名 カラム名の型, ……);
 mysql> CREATE TABLE items (id INT, name VARCHAR(255));
  -- ※ rails g model モデル名 でテーブルに紐づくモデルとマイグレーションファイルを作成/実行(rake db:migrate)するのと同義。
  -- ※ rake db:migrate で、 CREATE TABLE というSQL文が動いてる

-- テーブル構造(カラム名、カラム型、制約など)の確認
 mysql> SHOW columns FROM テーブル名;
 mysql> SHOW columns FROM items;

-- テーブル構造の変更(カラムの追加・修正・削除など)
 mysql> ALTER TABLE テーブル名 操作
 -- カラム追加
  mysql> ALTER TABLE テーブル名 ADD カラム名 カラム型;  -- 1つ追加
  mysql> ALTER TABLE テーブル名 ADD (カラム名 カラム型, ……);  -- 複数追加
  mysql> ALTER TABLE items ADD (price int, zaiko int);
 -- カラムの修正 (カラム型は変更がなくても、書く必要がある!)
  mysql> ALTER TABLE テーブル名 CHANGE 古いカラム名 新しいカラム名 新しいカラム型;
  mysql> ALTER TABLE items CHANGE zaiko stock int;
 -- カラムの削除
  mysql> ALTER TABLE テーブル名 DROP カラム名;
  mysql> ALTER TABLE items DROP stock;


 -- データの登録
  -- 全カラムに値を入れる場合
   mysql> INSERT INTO テーブル名 VALUES(1, ...);
  -- 特定カラムのみに値を入れる場合 (指定しないカラムはデフォルト値(NULL)が入る)
   mysql> INSERT INTO テーブル名(カラム名1, ...) VALUES(1, ...);  
   mysql> INSERT INTO items VALUES(1, "ペン", 120);
   mysql> INSERT INTO items(id, name) VALUES(2, "消しゴム");

 -- テーブルの全レコード表示
  mysql> SELECT * FROM items;

 -- データ更新
  mysql> UPDATE テーブル名 SET 変更内容 WHERE 条件;
  mysql> UPDATE items SET price = 100 WHERE id = 2;

 -- データ削除
  mysql> DELETE FROM テーブル名 WHERE 条件;
  mysql> DELETE FROM items WHERE id = 2;

-- mysqlを終了
 mysql> exit
SQL文 機能
SHOW DATABASES 文 DB一覧表示
CREATE DATABASE 文 DB作成
CREATE TABLE 文 テーブル作成
USE 文 DBを選択
USHOW TABLES文 DB内のテーブル一覧表示
ALTER TABLE 文 カラムの追加・修正・削除
INSERT 文 テーブルにデータ登録
UPDATE 文 テーブルのデータ更新
DELETE 文 テーブルのデータ削除
  • SQL文の終わりは ;を付ける。
    (忘れたら、SQLがまだ続くとみなされ、-> が表示されるので、;を入力すればOK)。
  • カラム型の(例) INT:数字、VARCHAR(●):最大●文字の文字列

クエリでのSQL文実行

  • 検索では、クエリから実行する。理由は、

    1. 検索では、SQL文が長くなるので、タイプミス時の修正が容易
    2. 検索結果の出力も長くなっても、SQL文が流れず、見やすい
    3. SQL文の文末の ;が不要。
  • クエリ(例えば、SequelPro)でのSQL文の実行方法は、上画面にSQL文を書き、現在を実行をクリックすると実行される。

  • SELECT句(テーブルの指定) + FROM句(取得するカラムの指定)が、検索の基本構造。

  • WHERE句: 取得するレコードを制限。条件式は、
    比較演算子(<、>、..)、
    AND演算子(and、or)、
    BETWEEN演算子(上限と下限の指定)、
    IN演算子(1つのカラムにリストを指定し、値がリストに含まれる時 true)、
    NOT演算子など。

  • ワイルドカード : 文字の代わりに使える記号のこと。*は全部の意味。

クエリでの検索
-- SQL文のコメントアウト

-- 検索の基本構造
 SELECT カラム名  -- 取得カラムの指定
 FROM テーブル名  -- 検索するテーブルの指定
 WHERE 条件     -- 取得レコードの制限
 -- (例)全カラムを取得、レコードの検索条件を指定
  SELECT *
  FROM users
  WHERE age <= 22 AND prefecture = "神奈川県"
 -- (例)
  SELECT *
  FROM users
  WHERE age <= 20 OR prefecture = "東京都"
 -- (例)
  SELECT *
  FROM users
  WHERE age BETWEEN 21 AND 24
 -- (例)
  SELECT *
  FROM users
  WHERE NOT prefecture = "東京都"
 -- (例)
  SELECT *
  FROM users
  WHERE prefecture IN ("東京都", "神奈川県")

-- 検索データの結合
 CONCAT(文字列1, 文字列2, ..) -- 文字列の連結
 -- (例)フルネームを出力する場合(通常、カラム名はSQL文になる)
  SELECT CONCAT(family_name, first_name)
  FROM users
 -- (例)フルネームを出力する場合(カラムに別名を付ける場合)
  SELECT CONCAT(family_name, first_name) AS "名前"  -- AS は省略可
  FROM users

-- 重複する行の除外
 SELECT DISTINCT カラム名
 -- (例)shiftsテーブルから、date = "2015-07-01"のカラム取得(重複するuser_idを除く)
  SELECT DISTINCT user_id
  FROM shifts
  WHERE date = "2015-07-01"

-- レコードのグループ化
 GROUP BY カラム名
 -- (例)shiftsテーブルからdate="2015-07-01"に誰がシフトに入ったかを取得("user_id"が同じものをグループ化し、"user_id"表示)
  SELECT user_id
  FROM shifts
  WHERE date = "2015-07-01"
  GROUP BY user_id

-- レコード数を数える
 SELECT COUNT(カラム名)
 SELECT COUNT(*)  -- 全部の値がNULLのレコードも含めた行数を取得
 -- (例)shiftsテーブルからdate="2015-07-01"に、誰が、何コマずつシフトに入ったかを取得(カラムにコマ数という別名を付ける)
  SELECT
    user_id,
    COUNT(*) "コマ数"
  FROM shifts
  WHERE date = "2015-07-01"
  GROUP BY user_id

-- テーブルの結合(テーブル1にテーブル2を結合(条件:テーブル1のカラム1がテーブル2のカラム2と一致するもの))
 FROM テーブル名1
 JOIN テーブル名2 ON テーブル名1.カラム名1 = テーブル名2.カラム名2
 -- (例)shiftsテーブルにusersテーブルを結合。(shiftsテーブルの"user_id"がusersテーブルの"id"と一致する行を結合)
  FROM shifts
  JOIN users ON shifts.user_id = users.id
 -- ↑は普通、簡略して書く!(テーブルに別名で扱う。テーブル名の頭文字が一般的。)
  FROM shifts s
  JOIN users u ON s.user_id = u.id
 -- まとめて書くと、
  SELECT
    CONCAT(family_name, first_name) "名前",
    COUNT(*) "コマ数"
  FROM shifts s
  JOIN users u ON s.user_id = u.id
  WHERE date = "2015-07-01"
  GROUP BY user_id

-- サブクエリ:検索結果を用いた検索(例えば、↑の検索に該当しないデータの取得とか)
 SELECT *
 FROM users
 WHERE id NOT IN (
   SELECT DISTINCT user_id
   FROM shifts
   WHERE date = "2015-07-01"
 )
IN演算子(カラム名に値1、もしくは、値2、...を含む場合true)
-- (値1, 値2, ……)部分がリスト
 WHERE カラム名 IN (1, 2, ……)
-- ↑と同義
 WHERE カラム名 = "値1" OR カラム名 = "値2" OR ...
  • CONCAT 関数: 複数の文字列を連結する関数(※文字列にNULLがあれば、結果はNULL)。
  • AS 句: CONCAT 関数での検索結果のカラム名は、SQL文のままで見にくいので、検索結果のカラム名の変更する時に使う。
  • DISTINCT キーワード: 指定カラムの値が重複する行を除外して取得。
  • GROUP BY 句: 指定カラムが同じ値のデータをグループにまとめる(表示されないデータもそのグループに保持されてる)。
    DISTINCTと似てるが、GROUP BYは、グループ単位で集計した結果を取得できることが利点。
  • COUNT 関数: グループ化したデータに使える集計関数。カラムを指定して使用することで、値がNULLでない行数を取得できる。
    GROUP BY句を併用すると、各グループが持つレコードの数を取得できる (※ グループ化データに使える集計関数は、他に、平均AVG、最大値MAX、最小値MINなど)。
  • JOIN 句: 指定テーブルの、カラムが同じデータを結合。FROM句の後に書き、結合する対象テーブルを指定。JOINにはINNER JOINLEFT JOINRIGHT JOINなど、結合法則の違うものがある。
  • サブクエリ: 検索結果を使って、別のSQL文を実行する仕組みのこと。WHERE句で使うことが多いが、SELECT句やFROM句でも使える。

DBとは

  • DB構成要素:3つ(サービスで扱う概念(エンティティ)、エンティティの属性、エンティティ間の関係性(リレーション))。
    エンティティ: サービスで管理する必要のある概念(情報、データ)のこと。テーブルにあたる。(例: ユーザー、投稿内容、コメント)
    エンティティの属性: エンティティが持つ個別情報のこと。カラムにあたる。(例: タイトル、説明、公開日、監督)
    リレーション: エンティティ間の関係性のこと。(例:映画-を監督)

  • 属性の中にはキー(テーブル内のレコード同士を識別するための被らないデータ)が存在する。 主キー 、 外部キー の2種類ある。
    主キー: テーブル内のレコードを判別のための識別子カラム(多くの場合、id)。
    外部キー: 関連する他のテーブルの主キー(カラム)と関係がある時に必要なカラム。他のテーブルのレコードとのリレーションを表すために使う。

DB設計

  • サービスに必要な情報の管理方法を決める作業。
  • 【DB設計の手順】
    1. 必要な機能を洗い出す。(ページの繋がりを把握から考えてみる)
    2. 洗い出した機能に、管理したいエンティティ(必要なデータ)を抽出する。
    3. データの持つ属性を決める。テーブルの分け方も考える。
      ※ データ変更・削除しないテーブルは、active_hashかenumを使えないか?も考えてみる。
    4. テーブルの繋がり(リレーション)を決める(ER図)。
      ER図とは? : Entity-Relationship Diagramの略、テーブル間の関係を視覚的に表した図(IE表記法 で書く)。

テーブル作成

モデル + マイグレーションファイルを作成

テーブル作成
$ rails g model モデル名

テーブル変更

マイグレーションファイルのみ作成。※ 名前は何でもok。

テーブル変更
$ rails g migration マイグレーション名
 $ rails g migration DropUser
# カラム追加
 $ rails g migration Addカラム名Toテーブル名 カラム名:
 $ rails g migration AddNameToUsers name:string
# カラム削除
 $ rails g migration Removeカラム名Fromテーブル名 カラム名:
# カラム名変更
 $ rails g migration RenameFrom変更前のカラム名To変更後のカラム名Onテーブル名
# カラムの Null制約変更
 $ rails g migration ChangeColumnToNotNull
# カラム変更
 $ rails g migration ChangeColumnToUser
# 外部キー削除
 $ rails g migration AddArticleToUsers article:references

# テーブル作成時に外部キーをreferences型カラムで保存
 $ rails g model モデル名(大文字単数) カラム名:references
 $ rails g model UserAccount user:references # 外部キー作成

$ rails g model User uuid:string:unique name:string

マイグレーションの実行・確認・取下げ・編集・削除

マイグレーション実行
$ rails db:migrate  # rails4までは、rakeコマンド
マイグレーションの確認
$ rails db:migrate:status
  up:実行済み、down:実行前の状態

※ schema_migrationsテーブルにもマイグレーションファイルのバージョンが保存されている(version:××××××)。
※ スキーマファイル(schema.rb):最新のテーブル一覧が記録されている。

実行済みのマイグレーション取下げ
$ rails db:rollback
 up  downの状態になる。
# 新しいものから2つのファイルを同時にロールバック
 $ rails db:rollback STEP=2

※ upの状態でマイグレーションファイル削除すると、エラーに繋がるので、down状態を確認してから削除!

マイグレーションをリセットして再実行(変更済みのマイグレーションを変更・削除するときとか。。)
# DBを削除後に、db/migrate/**.rb を古い順から実行
 % rails db:migrate:reset

# マイグレーションファイルの順番に問題がある場合、NGになることがある。
# そんな時は、分けて実行すると行ける場合がある。
 % rails db:reset   # db/schema.rbを元にDB作成
 % rails db:migrate
マイグレーションファイルの編集(テーブル作成)
def change
  create_table :hoges do |t|
    t.カラムの型   :カラム名
    t.string     :name
    t.text       :text
    t.references :user, foreign_key: true
    t.timestamps
  end
end
マイグレーションファイルの編集(カラム追加)
def change
  # カラム追加
  add_column    :テーブル名, :カラム名, :
  # カラム削除
  remove_column :テーブル名, :カラム名, :
  remove_columns :テーブル名, :カラム名, :カラム名,  :カラム名
  # カラム名変更
  rename_column :テーブル名, :変更前のカラム名, :変更後のカラム名
  # カラムの制約、オプションを変更
  change_column :テーブル名, :カラム名, :
  change_column :users, :name, :string, null: false
  change_column :users, :name, :string, limit: 10
  change_column :users, :uuid, :string, null: false, default: 0
  change_column :テーブル名, :カラム名, :, null: true  # NULL制約
  change_column :テーブル名, :カラム名, :, null: false  # NOT NULL制約
  change_column :テーブル名, :カラム名, :, index: true  # インデックス
  change_column :テーブル名, :カラム名, :, default: "piyo"  # デフォルト
  change_column :テーブル名, :カラム名, :string, limit: 12  # 長さ varchar(12)
  change_column :テーブル名, :カラム名, comment: "コメントです"  # コメント
  # テーブル作成
  create_table :テーブル名
  drop_table   :テーブル名
  # テーブル名変更
  rename_table  :現在のテーブル名,  :新しいテーブル名
  # インデックスをつける
  add_index  :テーブル名,  :インデックスを付けるカラム名 [, オプション])
  add_index :company, :company_name
  add_index :users, [:name, :name2]
  add_index :user_accounts, [:provider, :uid], :name => 'unique_provider_uid', :unique => true
  # インデックスを削除
  remove_index :テーブル名, :カラム名
  remove_index :company, name: :company_name_fk
  # 外部キーを追加
  add_reference  :テーブル名,  :リファレンス名 [, オプション]
  add_reference  :hoges,  :user, foreign_key: true
  add_reference :テーブ名, :カラム名, index: true

  add_foreign_key :対象テーブル, :指定先テーブル, :column :対象のカラム名
  add_foreign_key  :テーブル名,  :指定先のテーブル名 [, オプション]
  add_foreign_key :対象のテーブル, :指定先のテーブル, name: :foreign_keyの別名
  add_foreign_key :companies, :accounts
  add_foreign_key :companies, :accounts,  name: :huga_id  # 別名をつける場合
  add_foreign_key  :trades,  :users, column: :seller_id  # seller_idカラム対して外部キー制約をつける場合
  # 外部キーを削除
  remove_foreign_key  :テーブル名,  :指定先のテーブル名
  remove_foreign_key :users, column: :article_id  # 外部キー削除
  remove_reference :users, :article, foreign_key: true  # カラムも一緒に削除する場合
end

Rails5では、デフォルトでidはbigintで、参照元テーブルのカラムをintegerで作成するとforeign keyが作成できない。なので、テーブルのカラムをbigintで作成して、外部キーを設定すると良さそう!!

マイグレーションファイルでよく使うメソッド

メソッド名 概要
add_column カラム追加
remove_column カラム削除
remove_columns 複数カラムを削除
rename_column カラム名変更
change_column カラムの情報変更
create_table テーブル作成
drop_table テーブル削除
rename_table テーブル名変更
add_index インデックス追加
remove_index インデックス削除
add_reference 外部キー作成
remove_reference 外部キー削除
add_foreign_key 外部キー制約付与
remove_foreign_key 外部キー制約削除

カラムの型

カラム型 概要
string 文字(〜255文字)
text 文字(255文字〜)
integer 通常の整数
smallint 狭範囲の整数
bigint 大きな整数
float 小数点を含む数
decimal, numeric 大きな数 ※桁数などを指定する時
boolean 真/偽
time 時刻 12:00:00
date 日付 2020-01-01
datetime 日時 2001-01-01 01:01:00
timestamp タイムスタンプ
json JSONで保存
binary 画像などをバイナリデータにしたい時
references 外部キー

制約

特定のデータを保存さないためのバリデーション。

制約の種類 記述 概要
NOT NULL制約 null: false カラムに設定する制約。NULL(空)では保存さない(エラーにする)。必須項目に使う。
一意性制約 unique: true カラムに設定する制約。テーブル内で重複データの保存を禁止する
主キー制約 - フォーマットで設定。主キーの属性値が存在し、かつ、重複しないことを保証する。主キーへの NOT NULL制約 + 一意性制約と同義。
外部キー制約 foreign_key: true 外部キーに対応するレコードが存在することを保証する

※ 外部キーのカラム名を指定したい時

モデル
 belongs_to :user, foreign_key: :seller_id

インデックス

  • DBの機能の1つで、テーブル内のデータの検索速度向上のための仕組み。索引(目次)のイメージ。
  • 検索に使うカラムに使う。
  • インデックスを貼るメリット: データの読み込み/取得速度向上
    (牽引で目的の場所を見つけ、データを読み取るため)
  • インデックスを貼るデメリット: DBの容量が圧迫されるのと、書き込み速度低下
    (インデックスは、テーブルのデータとは別の領域に保存されるので、データを書き込む時(保存/更新)、テーブルへの書き込み、インデックスへの書き込みが必要なため)
  • 複数カラムにインデックスを貼れるが、カラム単体で検索する時は、検索速度は向上しないので、その複数カラムのセットでめっちゃ検索する場合だけ、複数カラムにインデックスを貼る
マイグレーションファイル
def change
  add_index :テーブル名, :カラム名
end

正規化

  • DBのデータ構造を無駄のないシンプルな構造にする手順。
  • 正規化後のDB構造を正規形、正規化していないDB構造を非正規形という。
  • 正規化順番: 非正規形 → 第1正規形 → 第2正規形 → 第3正規形 → ボイスコッド正規形 → 第4正規形 → 第5正規形。(第4正規形以降は、パフォーマンス低下の可能性があり、第三正規形に留めるのが一般的。)
  • 正規化のデメリット: 正規化は進めるほどに、パフォーマンスが低下する。小さなテーブルが増えていくことが理由。関連テーブルの検索のたびにSQL実行回数が増え、速度低下するため、第3正規形で終えることが多い。

【テーブルの問題点】
重複するカラム:冗長なDBになる。
1つのテーブルに複数の情報(エンティティ)が混在:可読性を含め、パフォーマンスが低下。
この問題を解決するのが正規化

正規化の順番

  • 第1正規形: 同一カラムが存在しない、かつ、レコードを構成する1つのマスに、値が1つずつ入る状態にすること。
  • 第3正規形: 第2、第3正規形は似ており、違いは複雑な概念のため、第2正規形は割愛。第3正規形は、主キーに依存、かつ、非キー属性(主キーでないもの)同士の依存がない状態にすること。
  • 混在したエンティティは、属性ごとにテーブルを分ける。

N+1 問題

モデルでDBへアクセス時、SQLが発行される。アソシエーションで、子モデルのインスタンスを複数取得する時は、N+1問題を考慮し、includesメソッドなどで解消する!

モデルでアソシエーション定義

  • 1 対 多: has_many :複数belongs_to :単数
  • 1 対 1: belongs_to :単数。所有(まるごと含む)場合は、has_one :単数
  • 多 対 多: 中間テーブルを作成。has_many through オプション↓↓
多対多モデル
has_many :相手のテーブル名, through: :中間テーブル名

# 中間テーブル
belongs_to :テーブル名1
belongs_to :テーブル名2


# アソシエーション定義すると、自動で以下のメソッドが使えるようになる。
 # 通常のレコード作成
 変数名1 = モデル名1.create(カラム名: "値", ..)  # 単体のインスタンス生成
 変数名2 = モデル名2.create(カラム名: "値", ..)
 # 多対多を定義したレコード作成
 変数名1.相手テーブル名2(複数形).create(カラム名: "値", ..)    # 多対多の関係を使って、変数名1に関係する相手テーブル名2のインスタンス生成
 # リレーション追加
 変数名1.相手テーブル名2(複数形) << 変数名2   # 後からインスタンス同士を関連付けも可能
 # 変数名1と関係する相手テーブル名2の配列に、モデル名1のインスタンス追加でリレーション生成。

 # リレーションを利用したレコード取得
 変数名.相手テーブル名(複数形)  # リレーションしている要素を全て出力(返り値は、関連付けられているインスタンスが出力される)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】FullCalendarを用いたスケジュール管理昨日の実装

目標

ezgif.com-video-to-gif (1).gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装

実装

1.Gemを導入

Gemfile
# 追記
gem 'jquery-rails'
gem 'fullcalendar-rails'
gem 'momentjs-rails'
ターミナル
$ bundle

2.application.scssを編集

application.scss
 *= require_tree .
 *= require_self
 *= require fullcalendar /*追記*/

3.application.jsを編集

application.js
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require jquery
//= require moment // 追記 
//= require fullcalendar // 追記
//= require_tree .

4.JavaScriptファイル作成・編集

ターミナル
$ touch app/assets/javascripts/calendar.js
calendar.js
$(function() {
  function eventCalendar() {
    return $('#calendar').fullCalendar({});
  }

  function clearCalendar() {
    $('#calendar').html('');
  }

  $('#calendar').fullCalendar({
    events: '/events.json',

    titleFormat: 'YYYY年 M月',
    dayNamesShort: ['', '', '', '', '', '', ''],

    header: {
      left: '',
      center: 'title',
      right: 'today prev,next',
    },

    defaultTimedEventDuration: '03:00:00',
    buttonText: {
      prev: '',
      next: '',
      prevYear: '前年',
      nextYear: '翌年',
      today: '今日',
      month: '',
      week: '',
      day: '',
    },
    timeFormat: 'HH:mm',
    eventColor: '#63ceef',
    eventTextColor: '#000000',
  });
});

【解説】

① turbolinks対策の為、カレンダーを読み込む関数と削除する関数をそれぞれ用意する。

function eventCalendar() {
  return $('#calendar').fullCalendar({});
}

function clearCalendar() {
  $('#calendar').html('');
}

② 日本語で表示する。

// カレンダー上部を年月で表示させる
titleFormat: 'YYYY年 M月',

// 曜日を日本語表示
dayNamesShort: ['', '', '', '', '', '', ''],

③ ボタンのレイアウトを整える。

header: {
  left: '',
  center: 'title',
  right: 'today prev,next',
},

④ イベントの設定をする。

// 終了時刻がないイベントの表示間隔を設定
defaultTimedEventDuration: '03:00:00',
buttonText: {
  prev: '',
  next: '',
  prevYear: '前年',
  nextYear: '翌年',
  today: '今日',
  month: '',
  week: '',
  day: '',
},

// イベントの時間表示を24時間にする
timeFormat: 'HH:mm',

// イベントの色を変える
eventColor: '#63ceef',

// イベントの文字色を変える
eventTextColor: '#000000',

4.モデルを作成

ターミナル
$ rails g model Event user_id:integer title:string body:text start_date:datetime end_date:datetime
~_create_events.rb
class CreateEvents < ActiveRecord::Migration[5.2]
  def change
    create_table :events do |t|
      t.integer :user_id
      t.string :title
      t.text :body
      t.datetime :start_date
      t.datetime :end_date

      t.timestamps
    end
  end
end
ターミナル
$ rails db:migrate

5.コントローラーを作成

ターミナル
$ rails g controller events new show edit my_calendar
events_controller.rb
class EventsController < ApplicationController
  before_action :set_event, only: [:show, :edit, :update, :destroy]

  def new
    @event = Event.new
  end

  def create
    @event = Event.new(event_params)
    @event.user_id = current_user.id
    @event.save ? (redirect_to event_path(@event)) : (render 'new')
  end

  def index
    @events = Event.where(user_id: current_user.id)
  end

  def show
  end

  def edit
    unless @event.user == current_user
      redirect_to root_path
    end
  end

  def update
    @event.update(event_params) ? (redirect_to event_path(@event)) : (render 'edit')
  end

  def destroy
    @event.destroy
    redirect_to my_calendar_path
  end

  def my_calendar
  end

  private

    def set_event
      @event = Event.find(params[:id])
    end

    def event_params
      params.require(:event).permit(:user_id, :title, :body, :start_date, :end_date)
    end
end

6.ルーティングを追加

routes.rb
# 追記
resources :events
get 'my_calendar', to: 'events#my_calendar'

7.json.jbuilderファイルを作成・編集

index.json.jbuilder
json.array!(@events) do |event|
  json.extract! event, :id, :title, :body
  json.start event.start_date
  json.end event.end_date
  json.url event_url(event)
end

【解説】

indexアクションで抽出したレコードを繰り返し処理し、配列を作成する。

json.array! @category_children do |children|

② 各データを で作成した配列に格納する。

json.extract! event, :id, :title, :body
json.start event.start_date
json.end event.end_date
json.url event_url(event)

8.ビューを編集

new.html.slimを編集

① HTMLを作成する。

new.html.slim
.row
  .col-xs-3

  .col-xs-6
    = form_with model: @event, url: events_path, local: true do |f|

      = f.label :title, 'スケジュール名'
      br
      = f.text_field :title, class: 'form-control'
      br

      = f.label :body, 'スケジュール内容'
      br
      = f.text_area :body, class: 'form-control'
      br

      = f.label :start_date, '開始日時'
      br
      = f.datetime_select :start_date, {}, class: 'form-control datetime-form'
      br
      br


      = f.label :end_date, '終了日時'
      br
      = f.datetime_select :end_date, {}, class: 'form-control datetime-form'
      br
      br

      = f.submit '新規登録', class: 'btn btn-success btn-block'

  .col-xs-3

② CSSでデザインを整える。

application.scss
// 追記
.datetime-form {
  display: inline-block;
  width: auto;
}

show.html.slimを編集

show.html.slim
.row
  .page-header
    h3
      | スケジュール管理

.row
  #calendar
  br

.row
  table.table.table-bordered
    tbody
      tr
        th.active
          | スケジュール名
        td
          = @event.title
      tr
        th.active
          | スケジュール内容
        td
          = @event.body
      tr
        th.active
          | 開始日時
        td
          = @event.start_date.to_s(:datetime_jp)
      tr
        th.active
          | 終了日時
        td
          = @event.end_date.to_s(:datetime_jp)

.row
  - if @event.user === current_user
    .pull-right
      = link_to 'スケジュール登録', new_event_path, class: 'btn btn-success'
      = link_to '編集', edit_event_path(@event), class: 'btn btn-primary'
      = link_to '削除', event_path(@event), method: :delete, data: { confirm: '本当に削除しますか?' }, class: 'btn btn-danger'

edit.html.slimを編集

edit.html.slim
.row
  .col-xs-3

  .col-xs-6
    = form_with model: @event, url: event_path(@event), local: true do |f|

      = f.label :title, 'スケジュール名'
      br
      = f.text_field :title, class: 'form-control'
      br

      = f.label :body, 'スケジュール内容'
      br
      = f.text_area :body, class: 'form-control'
      br

      = f.label :start_date, '開始日時'
      br
      = f.datetime_select :start_date, {}, class: 'form-control datetime-form'
      br
      br


      = f.label :end_date, '終了日時'
      br
      = f.datetime_select :end_date, {}, class: 'form-control datetime-form'
      br
      br

      = f.submit '保存', class: 'btn btn-primary btn-block'

  .col-xs-3

my_calendar.html.slimを編集

my_calendar.html.slim
.row
  .page-header
    h3
      | マイカレンダー

.row
  = link_to 'スケジュール登録', new_event_path, class: 'btn btn-success'

  #calendar
  br

注意

turbolinksを無効化しないとカレンダーが表示されない事があるので、必ず無効化しておきましょう。

turbolinksを無効化する方法

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

Rails migration

Railsのmigrationを触るのは初めてだったので、勉強備忘録のために簡単に書きとどめてみる。

migrationについて

マイグレーションは、データベーススキーマの継続的な変更 を、統一的かつ簡単に行なうための便利な手法。

migrationファイルを作成する

サンプルとしてCompanyテーブルを準備

カラム名 データ型
id integer
name string
description text
created_at datetime
updated_at datetime
$ rails g migration CrateCompany

Active Recordにはマイグレーションの実行順序をファイル名のタイムスタンプでの表示を自動でやってくれる。
作成されたファイルはdb/migrateディレクトリに保存される。

作成されたファイル

class CrateCompany < ActiveRecord::Migration[5.2]
    def change
    end
 end

ActiveRecord::Migration[]にはバージョンが入る。

chengeの代わりにupとdownを使うことができる。
upメソッドにはmigrationする時の内容、downメソッドにはrollbackする時の内容を書く。

class CrateCompany < ActiveRecord::Migration[5.2]
    def up
    end

    def down
    end
end

変更を加える

マイグレートファイルを作成しrails db:migrateを打てば実行してくる。
また直前の変更を取り消すためにはrails db:rollback

テーブルを作成する

class CrateCompany < ActiveRecord::Migration[5.2]
    def change 
     create_table :companies do |t|
    t.string :name
      t.text :description
      t.timestamps
    end
  end
end

このコマンドを打てば自動で上記のようなもの作成してくれる

$ rails g migration CrateCompany name:string description:text

テーブルを変更する

class ChangeTableCompany < ActiveRecord::Migration[5.2]
    def change 
     change_table :companies do |t|
     t.remove :name
     t.string :root_number
     t.index :root_number
     t.rename :description, :description_note
  end
  end
end

上のマイグレーションではnameカラムが削除され、stringカラムであるroot_numberが作成されてインデックスがそこに追加されます。そして最後にdescriptionカラムをリネームしている。

カラムを変更する

change_column :companies, :root_number, :text

モデル名のroot_numberのカラムをtextに変更。

ここでの注意点だが、change_columnchangeメソッドではロールバックすることができない。
changeメソッドではサポートさていないとのこと。
changeでサポートされているマイグレーション定義は

add_column
add_foreign_key
add_index
add_reference
add_timestamps
change_column_default (:fromと:toの指定は省略できない)
change_column_null
create_join_table
create_table
disable_extension
drop_join_table
drop_table (ブロックを渡さなければならない)
enable_extension
remove_column(型を指定しなければならない)
remove_foreign_key(2番目のテーブルを指定しなければならない)
remove_index
remove_reference
remove_timestamps
rename_column
rename_index
rename_table

もしロールバックする場合はup,downメソッドであればchange_columnを使用可能。

class ChangeColumnCompany < ActiveRecord::Migration[5.2]
    def up
     change_column :companies, :root_number, :text
    end

    def down
     change_column :companies, :root_number, :string
    end
end

まだまだmigrationについては知らないことばかりだが、身につけていきたい。

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

Your Ruby version is 2.4.6, but your Gemfile specified 2.6.4

マイグレーションファイルを作成しようとしたら、RubyのバージョンがGemfileで指定しているものと違うと言われ、実行できなかった。

$ bin/rails g migration hoge
Your Ruby version is 2.4.6, but your Gemfile specified 2.6.4

rbenvがインストールされていることを確認。

$ which rbenv
/usr/local/bin/rbenv

指定されたRubyのバージョンをインストール。

$ rbenv install 2.6.4
Downloading openssl-1.1.1g.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/ddb04774f1e32f0c49751e21b67216ac87852ceb056b75209af2443400636d46
Installing openssl-1.1.1g...
Installed openssl-1.1.1g to /Users/tamu/.rbenv/versions/2.6.4

Downloading ruby-2.6.4.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.4.tar.bz2
Installing ruby-2.6.4...
ruby-build: using readline from homebrew
Installed ruby-2.6.4 to /Users/tamu/.rbenv/versions/2.6.4

インストールされたことを確認。

$ rbenv versions
  system
* 2.4.6 (set by /Users/tamu/.rbenv/version)
  2.6.4

今回は、特定のディレクトリ配下にのみ適用したかったので、以下のように指定。

$ rbenv local 2.6.4

切り替わったことを確認。

$ ruby -v
ruby 2.6.4p104 (2019-08-28 revision 67798) [x86_64-darwin18]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

migrateファイルの変更(rails db:rollback)

こんにちは、tt_tsutsumiです。
今回はmigrateファイルの変更方法についてです。
こちらの記事が何方かのお役に立てると嬉しいです。

アプリケーション作成中にカラムの変更や追加を行った時に
普通のコードの書き方と違いが生まれたので困惑しました。
その際に自分が行った工程と流れの記載を致します。

1. rails db:migrate:status

まずは現在作成しているアプリケーションのmigrateの稼働状況の確認を行いましょう。
コンソールにて下記コードを入力します。

$ rails db:migrate:status

Status   Migration ID    Migration Name
--------------------------------------------------
   up     year/month/day  Devise create users
   up     year/month/day  Create spots

※ year/month/dayの部分はご自身で作成された日付等が表示されます

上記でstatusの部分がupになっている場合、migrateファイルは稼働しております。
この際にmigrateファイルの変更や追加を行っても反映されません。

2. rails db:rollback

migrateファイルの稼働状況をdownします。

$ rails db:rollback
$ rails db:migrate:status

Status   Migration ID    Migration Name
--------------------------------------------------
   up     year/month/day  Devise create users
   down   year/month/day  Create spots

ここで重要なのは rails db:rollback です!!
このコードを打つことにより稼働しているmigateファイルをdownさせる事ができます。

※ 注意点としてこちらのコードはファイルを1つずつしかdownに出来ません。

3. rails db:migrate

downになっているのを確認してからmigrateファイルの変更や消去を行って下さい。
そして変更が終わったら保存をし、rails db:migrate を行います。

$ rails db:migrate
$ rails db:migrate:status

Status   Migration ID    Migration Name
--------------------------------------------------
   up     year/month/day  Devise create users
   up     year/month/day  Create spots

これでmigrateファイルの変更と保存は終了です。
ご覧いただきありがとうございました !!

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

Ruby on Rails 非同期通信(ajax)についての振り返り

カリキュラム学習・ポートフォリオ作成中、この機能を実装するのに苦戦したので、メモ。
非同期通信にしたかったのは以下の二点。

①特定の商品をお気に入り登録or削除する
お気に入りの作成。削除ボタン.gif

②お気に入り一覧から削除する
お気に入り一覧の削除ボタン.gif

前提条件

テーブル

schema.rb
  create_table "users", force: :cascade do |t|
()
  end
 create_table "products", force: :cascade do |t|
()
  end

  create_table "favorites", force: :cascade do |t|
    t.integer "user_id", null: false
    t.integer "product_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

モデル(アソシエーション)

favorite.rb
belongs_to :user
belongs_to :product

def favorited_by?(user)
  Favorite.where(user_id: user.id).exists?
end
user.rb
has_many :favorites, dependent: :destroy
product.rb
has_many :favorites, dependent: :destroy

def favorited_by?(user)
  Favorite.where(user_id: user.id).exists?
end

ルート

rutes.rb
resources :products do
 resource :favorites, only: [:create, :destroy]
end

実装①(商品のお気に入り追加・削除)

productsコントローラは以下の通り

products_controller.rb
def show
  @product = Product.find(params[:id])
end

次にお気に入りボタンをパーシャル化。(userディレクトリ内に作成)

views/users/products/_favorite_button.html.erb
<% if product.favorites.where(user_id: current_user.id).exists? %>
  <%= link_to "お気に入りから削除",  users_product_favorites_path(product_id: product.id), method: :delete, remote: true %>
<% else %>
  <%= link_to "お気に入りに追加", users_product_favorites_path(product_id: product.id), method: :post, remote: true %>
<% end %>

ここでremote: trueを付けることで、非同期通信が可能になる。

products/show.html.erbで呼び出し

views/users/products/show.html.erb
<div id="favorites_buttons_<%= @product.id %>">
  <%= render 'users/products/favorite_button', product: @product %>
</div>

次はfavoritesのコントローラ。

fovorites_controller.rb
  def create
    @product = Product.find(params[:product_id])
    favorite = current_user.favorites.new(product_id: @product.id)
    favorite.save
  end

  def destroy
    @product = Product.find(params[:product_id])
    @favorites = current_user.favorites
    favorite = current_user.favorites.find_by(product_id: @product.id)
    favorite.destroy
  end

ここでリダイレクト先を指定しなければ、JSの処理を探しに行ってくれる。
最後に作成・削除した場合のJS処理を作成する。

views/users/favorites/create.js.erb
$("#favorites_buttons_<%= @product.id %>").html("<%= j(render 'users/products/favorite_button', product: @product) %>");
views/users/favorites/destroy.js.erb
$("#favorites_buttons_<%= @product.id %>").html("<%= j(render 'users/products/favorite_button', product: @product) %>");

show.html.erb内の、idで設定した範囲のみが、処理後書き換えられるようになる。

実装②(お気に入り一覧から削除)

fovoritesのコントローラは以下の通り作成

favorites_controller.rb
def index
  @favorites = current_user.favorites
end

非同期通信したい範囲をパーシャル化。

view/users/favorites/_form.html.erb
<% if favorites.present? %>
  <table class="table">
    <thead>
      <tr>
        <th colspan="3">商品</th>
      </tr>
    </thead>
    <tbody>
      <% favorites.each do |f| %>
        <tr>
          <td>
            <%= attachment_image_tag f.product, :image, :fill, 80, 80, format: 'jpeg', fallback: "no_image.jpg", size: '80x80' %>
          </td>
          <td><%= link_to f.product.name, users_product_path(f.product.id) %></td>
          <td><%= link_to "お気に入りから削除",  users_product_favorites_path(product_id: f.product.id), method: :delete, remote: true %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
<% else %>
  <h3>
    お気に入りリストがありません。<br>
    商品ページから追加してみましょう
  </h3>
<% end %>

fovorites/index.html.erbで呼び出し

<div class="row">
  <div class="col-sm-6 offset-3" id="favorites_index">
    <%= render 'users/favorites/form', favorites: @favorites %>
  </div>
</div>

JSの処理に以下の1行を追加

views/users/favorites/destroy.js.erb
$("#favorites_index").html("<%= j(render 'users/favorites/form', favorites: @favorites) %>");

これでお気に入り一覧にも非同期機能が実装できた。

実装した上での反省点・感想

  • JSに記載しているインスタンス変数はそれぞれcreate・destroyメソッドから確認しているため、そっちでも定義してあげないといけないということが身にしみた。(考えてみれば当たり前だったが)
  • 実装①についてはググればよく出てくるが、②は見かけないので自力で 解けて感動した。ただ流れを理解していれば迷うとこでも無かったなと感じた。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpec実行時の便利なオプションまとめ

Rails開発にRSpecによるテスティングを利用している方は多いかと思います。
となると、RSpecの実行効率が日々の生産性に影響を与えていると言っても過言ではないかもしれません。

RSpecには効率的にテストを実行できるオプションが多く用意されているため、私が特に便利だと感じたオプションに絞って紹介します。

失敗したテストのみを実行する

--only-failures

1件でも失敗したらテストをやめる

--fail-fast

タグがついたテストのみを実行する

--tag

テスト結果をドキュメント形式で表示する

--format documentation

テスト一覧を出力する

--dry-run

テスト時間を計測する

--profile

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

JWTを用いたTokenAPIの実装(Rails)

JWTとは

JSON Web Tokenの略称です。
今回はJWTを用いたTokenAPIをRailsに実装するのが目的なのでJWTについては簡単な説明だけにします。
TokenAPIは単純にemail,passwordでログインチェックを行って該当ユーザーが存在する時にそのユーザーのTokenを返すAPIです。
詳細内容については以下のリンクを参考にしてください。
https://openid-foundation-japan.github.io/draft-ietf-oauth-json-web-token-11.ja.html

JWT構成

ヘッダ、ペイロード、署名の3つのパートになっててそれぞれBase64でエンコードされている。

ヘッダ
{
  "typ":"JWT",
  "alg":"HS256"
}
ペイロード
{
  "sub": "1234567890",
  "iss": "John Doe",
  "aud": "audience",
  "exp": 1353604926
}
署名
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

3つのパートは . (ドット) で結合されている。

ヘッダ・ペイロード・証明を簡単に作ってみたいなら以下のリンクでできます。
https://jwt.io/#debugger

JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyIiwic3ViIjoidGVzdCIsImF1ZCI6ImF1ZGllbmNlIiwicGFyYW1zIjoie1wiZW1haWxcIjogXCJ0ZXN0QGdtYWlsLmNvbVwiLCBcInBhc3N3b3JkXCI6IFwicGFzc3dvcmRcIn0iLCJleHAiOiIxNTkzODM2ODA2In0.4fC4yLEmYTjiwaXk3R_AUUPEQSuI_ARmkoMqosWEJ-c

JWTクレーム

JWT クレームセットは JSON オブジェクトであり, それぞれのメンバは JWT として送られるクレームである. JWT クレームセット内のクレーム名は一意でなければならない
今回は以下の4つのクレームを使います。

"iss" (Issuer) クレーム:JWTの発行者の識別子
"sub" (Subject) クレーム:JWTの主語となる主体の識別子
"aud" (Audience) クレーム:JWTを利用することが想定された主体の識別子一覧
"exp" (Expiration Time):JWTの有効期限

APIプロジェクト生成

console
$ rails new jwt --api

Gemfileにjwt追加

gem 'jwt'

Jwt認証チェックモジュール作成

app/controllers/concerns/signature.rb
module Signature
  extend ActiveSupport::Concern

  def verify_signature
    render status: 401, json: { message: '認証エラー'} if request.headers['jwt-signature'].blank?

    request_params = JSON.parse(request.body.read)

    @signature ||= JwtSignature.new(jwt: request.headers['jwt-signature'])
    @signature.verify!(params: request_params)
  rescue JwtSignature::InvalidSignature
    render status: 401, json: { message: '認証エラー'}
  end
end

Jwt認証処理モデル作成

app/models/jwt_signature.rb
class JwtSignature
  class InvalidSignature < StandardError; end
  ALGORITHM = 'HS256'
  ISSUER = 'user'
  AUDIENCE = 'audience'
  SUB = "test"
  TOKEN_TYPE = 'JWT'
  # SECRET_KEYは重要なので環境ごとに定義して安全に管理してください〜
  SECRET_KEY = '1gCi6S9oaleH22KWaXyXZAQccBx4lUQi'

  def initialize(jwt:)
    @jwt = jwt
  end

  def verify!(params:)
    raise InvalidSignature unless valid_payload? && valid_params?(params: params) && valid_header?
  end

  private

  def valid_payload?
    return false unless jwt_payload['iss'] == ISSUER

    return false unless jwt_payload['sub'] == SUB

    return false unless jwt_payload['aud'] == AUDIENCE

    true
  end

  def valid_header?
    return false unless jwt_header['alg'] == ALGORITHM

    return false unless jwt_header['typ'] == TOKEN_TYPE

    true
  end

  def valid_params?(params:)
    JSON.parse(jwt_payload['params']) == params
  end

  def jwt_header
    @jwt_header ||= decoded_jwt.second
  end

  def jwt_payload
    @jwt_payload ||= decoded_jwt.first
  end

  def decoded_jwt
    @decoded_jwt ||= JWT.decode(@jwt, SECRET_KEY, true, algorithm: ALGORITHM)
  rescue JWT::DecodeError
    raise InvalidSignature
  end
end

JWT.decodeはHashのArrayを返すので
decoded_jwt.firstでPayload、decoded_jwt.secondでHeaderを取得
スクリーンショット 2020-07-04 13.55.00.png

User Table作成

app/models/user.rbが生成される

$ rails g model User name:string email:string token:string expired_at:datetime
$ rails db:migrate

routes.rbにAPI追加

routes.rb
Rails.application.routes.draw do
  post 'tokens/create'
end

Token Controller作成

tokens_controller.rb
class TokensController < ActionController::API
  include Signature

  # createの時だけ認証を確認する
  before_action :verify_signature, only: %i(create)

  def create
    # 今回は認証確認処理だけ実装
    user = User.find_by(email: params[:email], password: params[:password])

    return render status: 400, json: { message: 'ユーザーが存在しません。' } unless user

    # Tokenが存在しない場合は更新
    if user.token.blank?
      user.token = SecureRandom.uuid
      user.save
    end
    render status: 200, json: { name: user.name, email: user.email, token: user.token }
  end
end

テストユーザー登録

$rails c

irb(main):001:0> User.new(name: 'test', email: 'test@gmail.com', password: 'password')
   (0.5ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: "test", email: "test@gmail.com", password: [FILTERED], token: nil, expired_at: nil, created_at: nil, updated_at: nil>
irb(main):002:0> User.new(name: 'test', email: 'test@gmail.com', password: 'password').save
   (0.1ms)  begin transaction
  User Create (0.9ms)  INSERT INTO "users" ("name", "email", "password", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "test"], ["email", "test@gmail.com"], ["password", "password"], ["created_at", "2020-07-04 02:30:17.701626"], ["updated_at", "2020-07-04 02:30:17.701626"]]
   (0.8ms)  commit transaction
=> true

JwtEncodedデータ生成

https://jwt.io/#debugger
上記のリンクからJwtEncodedデータを生成
Payload部分は自分が必要なJwtクレームとパラメータを追加
paramsにemail, passwordを追加してAPIのパラメーターのJsonとPayloadのJsonを比較している
VERIFY SIGNATUREのSecurityキーも自分のSecurityに変更(JwtSignatureモデルのSECRET_KEY使用)
満期時間を設定したい場合はexpにUnixtimeを設定

unixtime(現在時間から5分後)
irb(main):040:0> (Time.now + 300).to_i
=> 1593838401

スクリーンショット 2020-07-04 13.49.22.png

Postmanで実行してみる

Headerに「jwt-signature」を追加してJwtEncodedデータを追加

JwtEncodedデータ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyIiwic3ViIjoidGVzdCIsImF1ZCI6ImF1ZGllbmNlIiwicGFyYW1zIjoie1wiZW1haWxcIjogXCJ0ZXN0QGdtYWlsLmNvbVwiLCBcInBhc3N3b3JkXCI6IFwicGFzc3dvcmRcIn0iLCJleHAiOiIxNTkzODM4NDAxIn0.dSNqdhHBJKUJHnJa_2sS_3Qr4oNNdr5MKFx5ufwqLv4

スクリーンショット 2020-07-04 13.48.47.png

Bodyにjson形式でemail, password追加

json
{ "email": "test@gmail.com", "password": "password"}

スクリーンショット 2020-07-04 11.47.42.png

実行

認証確認後無事にtokenが取得できました〜
スクリーンショット 2020-07-04 11.50.29.png

同じ状態で5分後実行

expに5分を設定したので5分後には認証失敗
スクリーンショット 2020-07-04 13.29.26.png

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

【Rails】データベースのテーブルやカラムを確認する

schema.rbを見る

db/schema.rbを見ると視覚的にテーブルとカラム構造を確認できる。

db/schema.rb
ActiveRecord::Schema.define(version: 2020_07_04_034908) do

  enable_extension "plpgsql"

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

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

end

schema.rbとは

schema.rbはRailsによって自動的に作られるファイルで、現在のデータベースの状態が書かれている。
データベースに変更を加えた時、すなわちrails db:migraterails db:rollbackをした時に自動的に書き変えられる。

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

【Rails】jquery を使わずに javascript で flashメッセージをフェードアウトさせる

はじめに

Railsアプリを開発していてflashメッセージをフェードアウトさせたく、javascriptを用いて実装したので書く。

こういったやつ↓↓(Bootstrapのalertを適用させています)
スクリーンショット 2020-07-04 8.59.20.png

これをフェードアウトさせる

環境

Ruby: 2.5.1
Rails: 5.2.4.2

実装

実装イメージとしては徐々に透明度を高くしていき、最終的に非表示にします。

要素のstyle属性
①opacityの値を減少させていく
②opacityが0になったらdisplay: none; にする

viewファイル

erbはこのようになっている

_flash_messages.html.erb
<% flash.each do |message_type, message| %>
  <div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>

jsファイル

flash_message.js
// turbolinks:loadでページ読み込み時に実行
document.addEventListener('turbolinks:load', () => {

  // flashメッセージ要素を取得
  const flashMessage = document.querySelector('.alert');

  // フェードアウトさせる(徐々に透過し,非表示にする)関数を定義
  const fadeOutFlashMessage = () => {
    // setIntervalを特定するために返り値を変数timer_idに格納
    const timer_id = setInterval(() => {
      // flashメッセージのstyle属性 opacityを取得
      const opacity = flashMessage.style.opacity;

      if (opacity > 0) {
      // opacityの値が0より大きければ0.02ずつ値を減少させる
        flashMessage.style.opacity = opacity - 0.02;
      } else {
        // opacityの値が0になったら非表示に
        flashMessage.style.display = 'none';
        // setIntervalをストップさせる
        clearInterval(timer_id);
      };
    }, 50); // 今回は0.05秒ごとにsetIntervalを実行
  }

  // flashメッセージがある場合のみ実行
  if (flashMessage !== null) {
    // style属性opacityをセット
    flashMessage.style.opacity = 1;
    // 今回は表示から3秒後に上記で定義したフェードアウトさせる関数を実行
    setTimeout(fadeOutFlashMessage, 3000);
  };
});

参考文献

下記記事を参考にさせていただきました。
ありがとうございました。

turbolinksチートシート
【JavaScript】Railsのflashをフェードアウトして消す方法

最後に

読んでいただきありがとうございます。
間違いや、より効率的な記述方法等あればご指摘いただけると非常に嬉しいです。

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

初心者がRails内のVueでライブラリを使わずに簡単なページネーションを作ってみた

Railsアプリ内でVueを使用し、SPAを実装しました。
ページネーション機能が欲しかったので、簡単に自分で実装してみました。
ライブラリを使うのが基本的だと思いますが、今回は練習したかったということでライブラリは使いませんでした。

プログラミング初心者かつVue自体も初めてなので間違いもだいぶあると思います。見つけたら指摘していただいたら嬉しいです。

version
Rails 6.0.3.2
ruby 2.6.3
@vue/cli 4.4.1

モデル

diary.rb
class Diary < ApplicationRecord
  # 省略
end

コントローラ

diaries.rb
def index
  diaries = Diary.all
  render json: diaries
end

コンポーネントのscript部分

今回は1ページ(1つのページネーションごと)に6つの要素を表示させることにしました。

表示分の要素をマウントできるようにする

まずコントローラから全ての要素を取得し、diariesとします。
のちの繰り返し構文で6つ表示させたいのでsliceメソッドを使い、diariesから6つの要素をとり、present_diariesとします。
sliceメソッドは第一引数+1番目の要素から第2引数番目の要素までを返します。

diaries.vue
<script>
  import axios from 'axios'
  export default {
    data: function() {
      diaries: [],
      present_diaries: []
    },
    mounted() {
      let that = this;
      axios.get('アクションへのパス').then(function(response) {
        that.diaries = response.data;
      }).then(function() {
        if (that.diaries.length > 6) {
          that.present_diaries = that.diaries.slice(0, 6);  // 6番目の要素まで取得
        } else {
          that.present_diaries = that.diaries
        }
      })
    }
  }
</script>

ページャーのカウントを算出

ページャーのカウントをいくつ出すかはcomputedで算出しました。
6つずつ出したいので取り出した要素を6で割ります。
要素が6つだったらページャーは1が表示されるようにします。
要素が7つなど、割り切れずに小数点が出た場合は切り上げてページャーのカウントを一つ増やします。
つまり、要素が7つだったらページャーのカウントは1と2が表示されるようにします。
ページャーの最大表示数は10としました。

diaries.vue
<script>
  // 省略

    // computedでページャーのカウントをいくつ出すか求めます
    compouted: {
      pageCount() {
        let count = this.diaries.length / 6  // 6つずつ表示したいので6で割ります
        let page_count = Math.ceil(count)    // ceilで切り上げます
        if (page_count > 10) {    // 10以上なら10を返します
          return 10
        } else {
          return page_count
        }
      }
    }
  }
</script>

選択されたページャーの番号によって要素が変わるようにする

ページャーの番号が押されたら、このイベントハンドラーが発動するとします。
何番が押されたかはテンプレートの方から渡し、引数countとします。
sliceメソッドを使い、引数(渡された数)に合わせて要素を取得します。
例えば、テンプレートから2が渡されたら、7 ~ 12の要素をとり、present_diariesに入れるようにします。

diaries.vue
<script>
 // 省略
  methods: {
    changeDiaries(count) {
      // 引数に合わせて要素を取得
      this.present_diaries = this.diaries.slice(count * 6 - 6, count * 6)
    }
  }
</script>

テンプレートで要素を表示する

v-forディレクティブで表示したい要素を複数表示します。
present_diariesをv-forに渡すことで、全ての要素ではなく、表示したい要素(ここでは6つ)のみを表示するようにします。

diaries.vue
<template>
  <div>
    <div v-for='diary in present_diaries' v-bind:key='diary.id'>
      {{diary}}
    </div>
  </div>
</template>

ページャーを表示する

computedで算出された数分の繰り返し処理を行います。3が算出されたら1,2,3と画面に表示されます。
また、v-onディレクティブでクリックに反応するようにし、先ほどのイベントハンドラを呼びます。
引数にそのボタンの数を与えます。

diaries.vue
<template>
  <div>
    // 省略
    <div v-for='count in pageCount'>
      <input type='button' v-bind:value='count' v-on:click='changeDiaries(count)'>
    <div>
  </div>
</template>

あとはcssなどで整えてください。
こんな感じで簡単なものは一応形になりました。

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

【Nuxt/Rails】axiosとdevise_token_authを使ってPOSTした実装

Nuxt.jsとRuby on Railsでaxiosとdevise_token_authを利用してPOSTする時に、地味に詰まってしまったので備忘録がてらまとめます。

Ruby on Rails側の実装

devise_token_authの設定諸々は省きます。

以下のURLあたりが参考になりましたので、そちらをご覧いただけると良いかもしれません。

https://github.com/lynndylanhurley/devise_token_auth
https://qiita.com/Masahiro_T/items/6bc49a625b437a7c2f45
https://sainu.hatenablog.jp/entry/2018/08/11/194319

FormObjectを導入してるので、そちらも込みで載せます。

articles_controller.rb
class ArticlesController < ApplicationController
  def create
    @article = ArticleForm.new(article_params)
    if @article.save
      # 省略
    end
  end

  private

  def article_params
    json_request = ActionController::Parameters.new(JSON.parse(request.body.read))
    json_request.permit(
      :title,
      :description,
    ).merge(user_id: current_user.id)
  end
end
articles_form.rb
class ArticleForm
  include ActiveModel::Model
  attr_reader :title, :description

  validates :title, presence: true, length: { maximum: 50 }
  validates :description, length: { maximum: 300 }

  def initialize(article_params)
    @article = article_params
  end

  def save
    return false if valid?
    Article.create(@article)

    true
  end
end

これで、POSTするまでの準備が整いました。

補足

POSTされる時にJsonをごにょごにょすることになったので、JsonをParseする処理を書いてあげる必要がありました。(ここに凄くハマりました。)

def article_params
  json_request = ActionController::Parameters.new(JSON.parse(request.body.read))
  json_request.permit(
    :title,
    :description,
  ).merge(user_id: current_user.id)
end

以下の記事に救われた(というかJsonのParse部分はコピペ)ので載せておきます。

http://tabunmuri.hatenablog.com/entry/2015/07/02/rails%E3%81%A7json%E3%81%AB%E3%80%81permit%E3%81%A7%E8%A6%81%E7%B4%A0%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%97%E3%81%9F%E3%81%84%E6%99%82%E3%81%AE%E6%96%B9%E6%B3%95

Nuxt.js側の実装

Componentに書いた実装は省いて、Vuexの中でやってるaxiosでPOSTした部分の実装だけ切り取ります。

article.js
export const actions = {
  async postArticle({ dispatch }, article) {
    await this.$axios
      .post('/articles', article, {
        headers: {
          'Content-Type': 'multipart/form-data', // サムネ画像を送信する想定のため
          'access-token': this.state.user.userToken.accessToken,
          client: this.state.user.userToken.client,
          uid: this.state.user.userToken.uid,
        },
      })
      .then(() => dispatch('getArticleList'))
      .catch((error) => console.log(error))
  },
}

devise_token_authのtokenをCookieに詰めて、nuxtServerInit時にVuexに書き出す実装をしていて、リクエスト時にヘッダーにTokenを突っ込めるようにしました。

ちなみに、Ruby on Railsではありませんが、GOのechoを利用した時に似たような実装をしていて、その備忘録を以下にまとめています。

https://qiita.com/arthur_foreign/items/fadd784610d764419786

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

【開発ログ⑰】サイドメニューの表示と非表示

前提について

はじめまして、
プログラミングスクールに通っていたいりふねと申します。この記事は、スクールの課題である個人アプリの開発の記録を書くことで、自身のアウトプットに利用しています。もし、読んでいただけた方がいましたら、フィードバックをしていただけたら嬉しいです。

開発するのは「有給休暇管理ツール」です。仕様は過去記事をどうぞ。

アプリはデプロイまで行いますが、サービスとして提供するものではありません。あくまでも自学習の一環ですので、ご理解下さい。では本題へどうぞ。

今回の実施内容

作成中のアプリのビューのうち、サイドメニュー部分の表示と非表示を切り替えたいと言うのが、今回の内容になります。というのもこの個人アプリを見た方から「iPadのようなモバイル端末でも見られるようにしないと企業の管理者としては使いづらいのでは?」というフィードバックを頂いたためです。たしかに。。。
具体的には以下の手順で実装しました。

  • application.html.hamlを編集
  • サイドメニューのクラス名を変更
  • サイドメニューのCSSを変更
  • サイドメニューの表示非表示の切り替えボタンを設置
  • 表示と非表示を切り替えるjsファイルの作成

開発環境

  • Rails 5.2.4.3
  • ビューは、HamlとSCSSで作成
  • jQueryが使用できる状態
  • Herokuを使用してデプロイ済み

デモ

今回は、「jQuery sidebar」というサイドバーを設置するためのプラグインを使用します。まずは、どんな動きをするのか以下の画像でご確認下さい。実際に触ってみたい方は、参考記事jQuery Sidebar 3.3.2からご確認いただけます。
aef472e934fa4a602aeb2b30a9b125f5.gif

・・・おわかりいただけただろうか。

サイドメニューと言っても、上や下からもメニューを呼び出せるようです。今回実装したのは、左から出てくるメニューのみですが、記述を少し変えれば色々と使えそう。

参考記事

今回もいくつか参考記事を使用しました。
【jQuery】簡単に設置できる!横から出てくるサイドバーを実装できる「jQuery Sidebar」
【公式】jQuery Sidebar 3.3.2
【GitHub】jillix/jQuery-sidebar

application.html.hamlを編集

まずは、application.html.hamlに「jQuery Sidebar」プラグインを読み込ませる記述を追加していきます。4と5行目のscriptタグが追記箇所になります。

app/views/layput/application.html.haml
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/
    %title Backyard
    %script{src: "//code.jquery.com/jquery-1.10.2.min.js" }
    %script{src: "//jillix.github.io/jQuery-sidebar/js/handlers.js" }

scriptタグのsrc属性がスラッシュから記述されている点に違和感を感じると思います。こちらの引用元は先程のGitHub内の「demo.html」となります。
スクリーンショット 2020-07-04 0.04.34.png
元の記述では、スラッシュの前に「http:」が付いた状態でしたが、この状態ではどうしてもデプロイした際にプラグインが読み込まれないという状態になりました。これについては、こちらの参考記事を読ませていただきました。

herokuに上げたらニコニコ動画が取ってこれなくなった〜httpsは魔物でござるの巻〜

サイドメニューのクラス名を変更

今回サイドメニューは、部分テンプレートで切り出しています。このビューファイルの先頭部分に記入している一番の親要素にあたるdivタグのクラス名を「sidebar left」に変更します。

_side.html.haml
.sidebar.left ←作成中のサイドメニューの一番大きなdivタグを変更
  .sidebar__lists  ←クラス名をBEMで書いている方は2行目以降もsidebarに書き換え
    - current_user.branches.each do |branch|
      .list
        .list__top
以下省略

念のため、検証ツールで確認しておきます。以下のようなクラス名になっていればOKです。
スクリーンショット 2020-07-04 0.28.00.png

今回は、左から出現するサイドメニューを想定していますが、それ以外の場合は、「left」部分を「right」「top」「bottom」のいずれかに変更すればOKだと思います(未検証)。

サイドメニューのCSSを変更

サイドメニューを左側に固定するためにCSSの記述を変更します。今回は、必要箇所のみ紹介していますが、アプリに合わせてサイドメニューのCSSを適宜記述して下さい。

_side.scss
  .sidebar{
    position: fixed;
  }

  .sidebar.left {
    top: 0;
    left: 0;
    bottom: 0;
  }

この段階でビューを確認します。サイドメニューが左に固定され、メイン部分のビューが下に回り込んでいればOKです。
スクリーンショット 2020-07-04 0.41.05.png

なお、上、右、下からメニューを表示させたい場合はCSSの記述が異なってくるので、ご注意下さい。参考記事【jQuery】簡単に設置できる!横から出てくるサイドバーを実装できる「jQuery Sidebar」内にその他の場合のCSSの記述が掲載されています。リスト6と書かれたコード見本部分になります。

サイドメニューの表示非表示の切り替えボタンを設置

jQueryによって表示と非表示を切り替えますが、イベント発火させるためのボタンを設置します。カスタムデータ属性の記述がhamlは、少し特殊になります。

_mainheader.html.haml
.nav__ragistation
  = link_to "#", {class: "btn link", data:{action:'toggle', side:'left'}} do
    メニュー

また、今回は、サイドバーを出現させるとメインヘッダー部分のボタンが隠れてしまうので、サイドメニュー本体にも表示の切り替えボタンを設置します。全く同じ記述内容です。

_side.html.haml
.sidebar__closed
  = link_to "#", {class: "btn", data:{action:'toggle', side:'left'}} do
    メニューを閉じる

検証ツールで正しく属性が設定されているか確認します。
スクリーンショット 2020-07-04 0.50.51.png
以下のように確認できればOKです。hamlではなく、htmlで記載する場合のカスタムデータ属性はそのまま「data-action」や「data-side」と入力すれば良いようです。

こちらの記述もまた、上や右、下からメニューを表示させたい場合は、data-side属性がleft以外の値(right、top、bottom)になるので注意が必要です。

表示と非表示を切り替えるjsファイルの作成

app/asset/javascriptsディレクトリ内に新規で、「sidebar.js」というファイルを作成します。ファイル名はお好みで決めて大丈夫だと思います。jsファイルの中身は、GitHubのjsファイルの記述をまるごとコピーします。

app/asset/javascripts/sidebar.js
長いので、具体的な記述はカットします。
リンク内の149行全てをコピー&ペーストすればOKです。

動作確認

最後にブラウザで挙動を確認します。
3782ff711adde92a777ee1981bbe9df7.gif

まとめ

文字量が多くなりましたが、手順としてはそれほど難しくなかったように感じます。最終的にjsファイルを丸ごとコピーというのも下品だったと思うので、今後は内容の理解に勤めたいと思います。

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