20190627のRailsに関する記事は19件です。

オリアプ設計諸々  確定版

サービス概要

ブラック企業での経験を記事にして投稿、誰でも自由に閲覧できる。
記事には、qiitaのようにタグ(例として、業界や賃金の未払いなど、どんなブラック要素があったのか)をつけられるようにする。

ターゲット  

Aさん(10代後半~20代前半、就労経験なし。これから就職をするにあたり、どんなブラック企業、ブラック要素があるか知りたい。記事を読んでもらいたい人)
Bさん(20代半ば~30代前半、若手社員。自身の労働環境がブラックではないかと疑念を抱いている。記事を読んでもらいたい人)
Cさん(年齢は特に指定なし。ブラック企業での就業経験あり。ブラック企業での経験や問題などを多くの人に知ってもらいたいと思っている。記事を書いてもらいたい人)

ログインユーザーと非ログインユーザー

  • 非ログインユーザー
    • 投稿の閲覧
    • 検索
    • サイトに対する質問
    • ユーザー詳細(記事を書いた人の)
    • 感想の閲覧
  • ログインユーザー
    • 記事の投稿
    • 改稿
    • 削除
    • 閲覧履歴
    • 記事のブックマーク(お気に入り)登録
    • 自身の投稿の履歴
    • 感想の投稿

機能

  • 記事の投稿
  • 記事の閲覧
  • 記事の削除
  • 記事の改稿
  • 記事の検索
  • 記事の閲覧履歴
  • ログイン
  • ログアウト
  • 記事のお気に入り登録
  • 自身の投稿の履歴などを含むユーザーの詳細(「プロフィールや一言」の入力欄あり)
  • ユーザー詳細の閲覧
  • 記事への感想の投稿
  • 記事への感想の閲覧
  • 記事への感想の閲覧の制限(非ログインユーザーに対して)
  • サイトに対する質問等の投稿

必要なテーブル

  • usersテーブル
    • ユーザーのテーブル
  • work_experiencesテーブル
    • ブラック企業での経験の記事のテーブル
  • work_experience_comentsテーブル
    • ブラック企業での経験の記事への感想のテーブル
  • likesテーブル
    • お気に入りのテーブル
  • work_experience_recordsテーブル
    • ブラック企業での経験の記事の閲覧履歴のテーブル
  • faqsテーブル
    • よくある質問のテーブル
  • questionsテーブル
    • サイトに対する質問等のテーブル

テーブルの表

usersテーブルの表

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
ユーザーのid id integer あり Yes No
ユーザーの名前 name string あり limit No No
ユーザーのパスワード password_digest string あり unique:true, limit No No
ユーザーのメールアドレス email string あり unique:true No No
ユーザーのプロフィール profile text なし limit No No
ユーザーの投稿日時 created_at datetime あり No No
ユーザーの更新日時 updated_at datetime あり No No

work_experiencesテーブルの表

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
記事のid id integer あり Yes No
記事の題名 title string あり unique:true, limit No No
記事のタグ tag string なし limit No No
記事の本文 body text あり unique:true, limit No No
ユーザーのid user_id integer あり No Yes
記事の投稿日時 created_at datetime あり No No
記事の更新日時 updated_at datetime あり No No

work_experience_comentsテーブルの表

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
感想のid id integer あり Yes No
感想の本文 body text あり limit No No
記事のid work_experience_id integer あり No Yes
ユーザーのid user_id integer あり No Yes
感想の投稿日時 created_at datetime あり No No
感想の更新日時 updated_at datetime あり No No

likesテーブルの表

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
お気に入りのid id integer あり Yes No
記事のid work_experience_id integer あり No Yes
ユーザーのid user_id integer あり No Yes
お気に入りの投稿日時 created_at datetime あり No No
お気に入りの更新日時 updated_at datetime あり No No

work_experience_recordsテーブルの表

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
閲覧履歴のid id integer あり Yes No
記事のid work_experience_id integer あり No Yes
ユーザーのid user_id integer あり No Yes
閲覧履歴の投稿日時 created_at datetime あり No No
閲覧履歴の更新日時 updated_at datetime あり No No

faqsテーブルの表

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
faqのid id integer あり Yes No
faqの種類 faq_kind string あり No No
faqの投稿日時 created_at datetime あり No No
faqの更新日時 updated_at datetime あり No No

questionsテーブルの表

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
質問等のid id integer あり Yes No
メールアドレス email string あり No No
質問等の本文 body text あり limit No No
faqのid faq_id integer あり No Yes
質問等の投稿日時 created_at datetime あり No No
質問等の更新日時 updated_at datetime あり No No

アソシエーション

  • usersテーブルとwork_experiencesテーブルの関係は、1対多
  • usersテーブルとwork_experience_comentsテーブルの関係は、1対多
  • work_experiencesテーブルとwork_experience_comentsテーブルの関係は、1対多
  • work_experiencesテーブルとlikesテーブルの関係は、1対多
  • usersテーブルとlikesテーブルの関係は、1対多
  • work_experiencesテーブルとwork_experience_recordsテーブルの関係は、1対多
  • usersテーブルとwork_experience_recordsテーブルの関係は、1対多
  • faqsテーブルとquestionsテーブルの関係は、1対多

非ログインユーザーの感想の閲覧機能の制限

記事を未投稿のユーザーは、閲覧できる感想の数を制限することにする。

方向性としては、記事を未投稿のユーザーは最初の何件かのみを閲覧できるようにし、それ以上の感想を閲覧したい場合は、記事の投稿が必須であることをビューで表示する。

ただし、感想の投稿は問題なく使用できる。

おおよその実装手順は以下の通り。

  • 現在ログインしているユーザーをcurrent_userという変数に代入する
  • current_userに代入したユーザーに紐づいた記事があるか調べる
    • current_userのidカラムの値とwork_experiencesテーブルのuser_idカラムの値が一致するデータがあるか調べる
  • 無かった場合は、感想の閲覧部分のビューで表示件数に制限をかける。同時に感想の閲覧を制限なく使用する場合は、記事の投稿が必要だと表示する。
  • データがあった場合は、制限なく、全ての感想の閲覧ができる

indexについて

実際にインデックスを貼るカラム

usersテーブル

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
ユーザーのパスワード password_digest string あり unique:true, limit No No
ユーザーのメールアドレス email string あり unique:true No No

work_experiencesテーブル

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
記事の題名 title string あり unique:true, limit No No
記事のタグ tag string なし limit No No
ユーザーのid user_id integer あり No Yes

work_experience_comentsテーブル

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
記事のid work_experience_id integer あり No Yes
ユーザーのid user_id integer あり No Yes

likesテーブル

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
記事のid work_experience_id integer あり No Yes
ユーザーのid user_id integer あり No Yes

work_experience_recordsテーブル

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
記事のid work_experience_id integer あり No Yes
ユーザーのid user_id integer あり No Yes

faqsテーブル

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
faqの種類 faq_kind string あり No No

questionsテーブル

役割対象 カラム名 データ型 NOT NULL その他の制約 PK FK
faqのid faq_id integer あり No Yes

tagカラムに関して

railsでタグ機能を実装する

動的なタグ生成をするgem「acts-as-taggable-on」を使ってみました

Rails gem acts-as-taggable-onを使ってみた

rails5でタグ機能をacts-as-taggable-onを用いて実装する方法

上記の4つの記事を参考にする。

今回のオリアプではgemを使用する。

使用するgem名は「acts-as-taggable-on」。

必要な要件は以下の通り。

  • 使うテーブルは、work_experiencesテーブル
  • 新規投稿時にタグを追加する
  • 改稿時にタグも変更、更新できるようにする
  • タグは検索でも使用できるようにする
  • 投稿閲覧(詳細)、投稿一覧ページ等にタグのリンクを作る。リンクをふむと、そのタグが付けられた投稿の一覧が表示される。

実際の実装手順としては、おおよそ以下の通り。

  • gemのインストール
  • タグ用のテーブル作成及び、マイグレーションの適用
  • work_experiencesテーブルのモデルに「acts_as_taggable」を追記
  • work_experiencesのコントローラのStrong Parametersに「:tag_list」の追記
  • work_experiencesのモデルでransackの検索条件に「:tag_list」の追記
  • アクションで「params[:tag]」で条件分岐(ある場合は「Labor.tagged_with(params[:tag])」、ない場合は全記事を取得)
  • タグ用のフォームをビューに追加
  • リンク付きタグを投稿一覧等に追加
  • リンク付きタグ用のルーティングの追加

また、英文だがリファレンスサイトもあったため、これも適宜参考にする。

https://github.com/mbleigh/acts-as-taggable-on

その他今回のオリアプで使えそうなgemのまとめ

「Ruby on Rails5 速習実践ガイド」で使用したgemに関しては確定的に使用するので、ここではgem名だけ載せる。

  • gem 'slim-rails'
  • gem 'html2slim'
  • gem 'bootstrap'
  • gem 'rails_autolink'
  • gem 'ransack'
  • gem 'kaminari'
  • gem 'sidekiq'
  • gem 'webpacker'
  • gem 'mailcatcher'
  • gem 'rspec-rails'
  • gem 'factory_bot_rails'
  • gem 'pry-rails'

それ以外で使えそうなgemをここでは挙げる。ただし、まだ、使用するかどうかわからないので、gem名と何に対して使用するgemなのかを簡単に纏める程度とする。

  • devise
    • ログイン機能の実装を簡単にできるgem
  • better_errors
    • エラー画面に、デフォルトのエラー画面より詳しい情報を表示してくれるgem
  • acts-as-taggable-on
    • タグ機能の実装で使用するgem(ほぼ確実で使用する)
  • Active Admin
    • 管理画面作成に役立つgem
  • font-awesome-rails
    • font-awesome(を使えるようにするため)のgem
  • factory_girl_rails  - テスト用のデータが簡単に生成できるようになるgem
  • Faker
    • 「factory_girl_rails」と同様に一般的にありがちなテストデータを簡単に作成できるようになるgem
  • whenever
    • 定期実行ができるgem。オリアプで言えば、記事の投稿、改稿時に自動で下書きの保存をする処理に使用できるかもしれない。

現段階では、acts-as-taggable-on、font-awesome-railsの二つはほぼ確実に使用する。
他にも使えそうなgemがあれば適宜修正していく。

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

RSpecを起動ようとしたら、「unknown command: Cannot call non W3C standard command while in W3C mode」と言われて怒られた

導入

以下の書籍を読みながら、RSpecについて学んでいます。

現場で使える Ruby on Rails 5速習実践ガイド

写経しながら、RSpecでテストコードを書いて、いざはじめてのRSpecと思って、
ワクワクしながらコマンドを叩くと、以下のエラーが出た。

Selenium::WebDriver::Error::UnknownCommandError:
       unknown command: Cannot call non W3C standard command while in W3C mode

Google Chromeとchromedriverのバージョンを最新の75にアップデートしたことで、W3CモードがデフォルトONになったみたい。

解決策

オプションに「w3c:false」を渡してやると、うまく解決が出来た。
自分の場合はオプションの設定方法が分からなくてハマったけど、何とかテストコードが動いて、
All Greenになった!

RSpec.configure do |config|

  config.before(:each, type: :system) do
    # 修正前
    driven_by :selenium_chrome_headless

    # 修正後
    caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {"w3c" => false})
    driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400], options: { desired_capabilities: caps }
  end

参考にしたURL

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

jQueryとsave時のエラー

はじめに

  • 条件を満たしているはずなのに新規登録時に登録に失敗しましたと出る
  • 登録失敗時のメッセージ「登録に失敗しました」が消えずにずっと表示されたままになっている。

この二つの問題解決にあたるが,いざ取り組むとあっさり解決した。途中まで書いてしまっていたので,一応投稿しておく。

内容はかなりしょぼいです。

JavaScript

「登録に失敗しました」が5秒で消えない。
とりあえず各ファイルの記述を見ていく。

users_controller.rb
class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to root_path, success: '登録が完了しました'
    else
      flash.now[:danger] = "登録に失敗しました"
      render :new
    end
  end

  private
  def user_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation)
  end
end
application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Pictgram</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

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

  <body>
     <% flash.each do |key, value| %>
       <div class="alert alert-<%= key %>" role="alert"><%= value %></div>
     <% end %>
     ~
     ~

     <%= yield %>

     <script>
       $(function(){
         $(".alert").fadeOut(5000);
       });
     </script>
  </body>
</html>
group :development do
  # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '>= 3.0.5', '< 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
  gem 'pry-rails'
  gem 'pry-doc'
  gem 'pry-byebug'
  gem 'pry-stack_explorer'
  gem 'jquery-rails'
  gem 'bcrypt'
  gem 'carrierwave'
end
application.js
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require bootstrap-sprockets
//= require jquery
//= require_tree .

各記述に加え,もちろんbundle installも行っている。

そこで少し調べたところ,Gemfileの記述に

gem 'jquery-ui-rails'

application.jsに

application.js
//= require jquery_ujs

を加えることで解決することがあるらしいので試してみるとあっさり解決。
Rubyのバージョンが古かったのかな?また今度調べてみる。

ROLLBACK

一番の問題はここ。
条件を満たしているはずの新規登録時にターミナルに以下のメッセージが出る。

Started POST "/users" for ::1 at 2019-06-26 17:51:29 +0900
Processing by UsersController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"VKkdJMhrlEzltFp
cD4OJLPN/LhgoyBHmIRj7R6Y+1Z/vn2QXJ60dLptQuvzdmYi/FBCRlSHsBljz/BpW9
PaQQg==", "user"=>{"name"=>"***", "email"=>"***", "passeword"=>"***", "password_confimation"=>"[FILTERED]"}, "commit"=>"登録"}
Unpermitted parameters: :passeword, :password_confimation
   (0.5ms)  BEGIN
  ↳ app/controllers/users_controller.rb:8
   (0.3ms)  ROLLBACK
  ↳ app/controllers/users_controller.rb:8
  Rendering users/new.html.erb within layouts/application
  Rendered users/new.html.erb within layouts/application (2.1ms)
  User Load (0.2ms)  SELECT  `users`.* FROM `users` WHERE `users`.
`id` IS NULL LIMIT 1
  ↳ app/controllers/concerns/common_actions.rb:5
Completed 200 OK in 69ms (Views: 58.3ms | ActiveRecord: 1.1ms)


Started GET "/users/new" for ::1 at 2019-06-26 17:54:52 +0900
Processing by UsersController#new as HTML
  Rendering users/new.html.erb within layouts/application
  Rendered users/new.html.erb within layouts/application (2.3ms)
  User Load (0.4ms)  SELECT  `users`.* FROM `users` WHERE `users`.
`id` IS NULL LIMIT 1
  ↳ app/controllers/concerns/common_actions.rb:5
Completed 200 OK in 83ms (Views: 80.0ms | ActiveRecord: 0.4ms)

ここで以下の記述に注目した

(0.3ms)  ROLLBACK
  ↳ app/controllers/users_controller.rb:8
  Rendering users/new.html.erb within layouts/application
  Rendered users/new.html.erb within layouts/application (2.1ms)

何かがロールバックしている。追っていくとまずはusers_controller.rbの8行目に何かあるよと。

users_controller.rb
# 8行目↓
if @user.save

じゃあこの処理はどこへ行ってるのかというとnew.html.erbである。このファイル内の記述を見ていく。

new.html.erb
<div class="users-new-wrapper">
  <div class="container">
    <div class="row">
      <div class="col-md-offset-4 col-md-4 users-new-container">
        <h1 class="text-center text-white">Sign up</h1>
        <%= form_for @user do |f| %>
          <div class="form-group">
            <%= f.label :name, class: 'text-white' %>
            <%= f.text_field :name, class: 'form-control' %>
          </div>
          <div class="form-group">
            <%= f.label :email, class: 'text-white' %>
            <%= f.text_field :email, class: 'form-control' %>
          </div>
          <div class="form-group">
            <%= f.label :password, class: 'text-white' %>
            <%= f.passeword_field :password, class: 'form-control' %>
          </div>
          <div class="form-group">
            <%= f.label :password_confirmation, class: 'text-white' %>
            <%= f.password_field :password_confimation, class: 'form-control' %>
          </div>

          <%= f.submit "登録", class: 'btn-block btn-white' %>
        <% end %>
        <%= link_to 'ログインはこちら', login_path, class: 'text-white' %>
      </div>
    </div>
  </div>
</div>

パッと見では問題がなさそう。
そこで調べてみると

users_controller.rb
if @user.save

if @user.save!

へ変更してみるとエラー個所がわかるよという記事を見つけた。
早速変更してrailsサーバーを再起動,新規登録を行ってみるとターミナルに新しい動きがあった。

ActiveRecord::RecordInvalid (Validation failed: Password can't be
blank, Password is too short (minimum is 8 characters), Password i
s invalid):

app/controllers/users_controller.rb:8:in `create'

なにやらパスワードがおかしいのかなと思う。下記の通りにValidationを設定しており,条件は満たしているはずなのに「空白はダメだよ(8文字以下になっているよ)」と怒られている。

user.rb
class User < ApplicationRecord
  validates :name, presence: true,
             length: { maximum: 15 }
  VALID_EMAIL_REGEX = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/
  validates :email, presence: true,
             format: { with: VALID_EMAIL_REGEX }
  VALID_PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]{8,32}+\z/i
  validates :password, presence: true,
             length: { minimum: 8, maximum: 32 },
             format: { with: VALID_PASSWORD_REGEX }

  has_secure_password

  has_many :topics
end

ここでは特になにもなさそうなのでpasswordの定義を行っているnew.html.erbへ戻って問題を確認する。

new.html.erb
        <%= form_for @user do |f| %>
          ~
          ~
          <div class="form-group">
            <%= f.label :password, class: 'text-white' %>
            <%= f.password_field :passeword, class: 'form-control' %>
          </div>
          <div class="form-group">
            <%= f.label :password_confirmation, class: 'text-white' %>
            <%= f.password_field :password_confimation, class: 'form-control' %>
          </div>

ここで間違いに気がつく。一箇所passwordがpassewordになっている。これを修正するとあっさり登録完了。問題は解決した。

おわりに

次回こそは
- 投稿する画像の制限
- 非ログイン時の投稿ボタンの非表示
を実装していく。

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

[Ruby on Rails]メールの送信結果をブラウザ上で確認する

メール送信結果をブラウザ上で確認する方法です。
letter_openerというgemを使います。
本番環境では実際に送信することになるので、開発環境でのみ確認できる状態を作ります。

環境

  • MacOS 10.14.5(18F132)
  • ruby 2.3.7p456
  • rails 5.2.3

手順

1. letter_openerをインストール

今回は開発環境でのみ確認できる状態を作るため、groupの設定をします。
すでにdevelopmentの設定が出来ている場合は、gem名のみを追記してください。

Gemfile
group :development do
  gem 'letter_opener_web'
end

追記したらコマンド

$ bundle install

2. 各種設定ファイルの編集

  • config/routes.rb
config/routes.rb
# 追記
if Rails.env.development?
  mount LetterOpenerWeb::Engine, at: "/letter_opener"
end
  • config/envirronments/development.rb
config/envirronments/development.rb
#追記
ActionMailer::Base.delivery_method = :letter_opener_web

#他にaction_mailerの設定をしている場合はコメントアウトする
#config.action_mailer.delivery_method = :smtp
#config.action_mailer.smtp_settings = {
  #省略
#}

3. ブラウザから確認する

$ rails sでサーバーを起動して、ブラウザでlocalhost:3000/letter_openerにアクセスします。
このような画面が表示されればOKです。
スクリーンショット 2019-06-27 16.49.38.png

4. メールを送信する

メールを送信する処理を実行します。

5. 再度ブラウザから確認する

ブラウザでlocalhost:3000/letter_openerにアクセスして、メールを確認します。
※deviseのパスワード再設定メールを送ってみました。
スクリーンショット 2019-06-27 17.45.17.png

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

params[:id]とfind_byメソッドの意味~ヘルスケアwebサービスを自分で作る医者の日記~

progate 9回 ラスト

URLの指定で、他人の投稿にアクセスできてしまいのを防ぐ回

def ensure_correct_user
    @post = Post.find_by(id: params[:id])
    if @post.user_id != @current_user.id
      flash[:notice] = "権限がありません"
      redirect_to("/posts/index")
    end

find_byメソッドの意味がスッキリした
SQLの id というカラムから、params[:id]に合致するものを探し、
それをインスタンス変数として@post に渡している。
これは投稿そのもの、一列丸ごと 

ここでparams[:id]にする意味がわからなかったが

params[:id]はルートの指定を格納している
posts/1
みたいな

ユーザー各ページのURLを入れ込むことによって、ここにアクセスすることを防いでいる
と判明!!

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

配列によるメソッドの可変長引数!

可変長引数とは???

個数に制限のない引数のこと!
自分で、定義するメソッドで可変長引数を使いたい場合は、引数名の手順に*をつけます!

sample.rb
def メソッド名(引数1,引数2,*可変長引数)
  #メソッドの処理
end

可変長引数は配列として受け取ることができます!

具体例を見ていきましょう!
次のコードは、引数として渡されたメニューの名前を、注文していくメソッドです。

sample.rb
def order(*food)
  puts "#{food.join('と')},お願いします!"
end

order('ハンバーガー') #ハンバーガー,お願いします!
order('ハンバーガー','ポテト') #ハンバーガーとポテト,お願いします!
order('ハンバーガー','ポテト','コーラ') #ハンバーガーとポテトとコーラ,お願いします!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dependentはどうやって動いているのか?

後述する仕様を実現するためにactiverecordのdependent周りのソースを読んだのでまとめました。

前提

railsのアソシエーションにはdependentというオプションを設定することができます。
これによりレコードを削除した時に関連レコードに対して自動的に様々な動作をさせることができます。
例えばhas_manyの場合は下記を指定できます。

# レコード削除時に関連するbooksも削除する(callbackあり)
has_many :books, dependent: :destroy
# レコード削除時に関連するbooksも削除する(callbackなし)
has_many :books, dependent: :delete_all
# レコード削除時に関連するbooksの外部キーをnilに更新する
has_many :books, dependent: :nullify
# レコード削除時に関連するbooksが存在する場合例外を発生させる
has_many :books, dependent: :restrict_with_exception
# レコード削除時に関連するbooksが存在する場合errorsを追加して削除を失敗させる
has_many :books, dependent: :restrict_with_error

詳細はrailsドキュメント参照してください。
https://railsguides.jp/association_basics.html#dependent

やりたかったこと

(色々端折ってはいますが)下記の仕様を満たすモデルを作成しようとしていました。

  • お店には本を追加・削除できる。ただし、必ず1冊以上は本がある状態をキープする。
  • お店を削除した時は全ての本を削除する。

簡単に実装したものが下記。
ソースコメントに記載していますが、before_destoryの中で①「shopを削除してdependent: :destroyで削除されようとしている」のか、②「booksを単独で消そうとした」のかを判断する方法がぱっとわかりませんでした。

# お店
class Shop < ApplicationRecord
  has_many :books, dependent: :destroy
end

# 本
class Books < ApplicationRecord
  belongs_to :shop

  before_destroy :before_destroy_action

  def before_destroy_action
    # 最後の1冊の場合は削除できない。
    # ただし、お店ごと削除する場合はすべて削除する。 <- これどう判定するの????
    raise unless Books.not.where(id: id).exists?
  end
end

考えた解決案

dependent: :delete_allを使う

dependent: :destroyの部分をdependent: :delete_allを変更することでcallbackが呼ばれなくなるのでbefore_destroybooksを直接削除したときのことだけ考えれば良くなる。
これで動作は問題ないが、今後booksに他のcallbackが追加された時や子テーブルが追加されてdependent: :destoryとしたい時などに支障がでるためこの方法はパス。

# お店
class Shop < ApplicationRecord
  has_many :books, dependent: :delete_all
end

# 本
class Books < ApplicationRecord
  belongs_to :shop

  before_destroy :before_destroy_action

  def before_destroy_action
    # 最後の1冊の場合は削除できない。
    # shopを削除した場合はbefore_destroyは実行されないので考慮不要
    raise unless Books.not.where(id: id).exists?
  end
end

dependent: :destroyで削除する場合にshopレコードに判定するためのフラグを持たせる

下記のようにshopを削除する時にフラグを立ててbookbefore_destroy内でそのフラグで判定する。
この方法でも動作は問題ないが、そもそもこの微妙なフラグをわざわざ自前で定義する必要があるのか?railsのことだから判定する方法があるのではないか?と考えて保留にしました。

# お店
class Shop < ApplicationRecord
  before_destroy: :before_destroy_action
  has_many :books, dependent: :destroy

  def before_destroy_action
    @destroying = true
  end

  def destroying?
    @destroying
  end
end

# 本
class Books < ApplicationRecord
  belongs_to :shop

  before_destroy :before_destroy_action

  def before_destroy_action
    # shopを削除した場合は何もしない
    return if shop.destroying?

    # 最後の1冊の場合は削除できない。
    raise unless Books.not.where(id: id).exists?
  end
end

railsに組み込まれている機能を使って判定する

ということでrailsのソースを読んで使えそうな機能を探すことにしました。

destroyについて

最初にdestroyのソースを見てみました。
@destroyedってフラグを立てているじゃないか。これ使えないかな?
https://github.com/rails/rails/blob/93a6500baa6bbb331bb93ccdc14fdda5769f5ef9/activerecord/lib/active_record/persistence.rb#L172

結論を先に書きますがdependent: :destroyによる削除が先に動いてしまい、関連データのbefore_destroyが呼ばれる時には@destroyedはtrueになっていません。

dependentについて

動作を確認してdestroyより先にdependent: :destroyが動いていることはわかりましたが、そもそもdependentって何をしているんだろうということでソースをみてみました。

https://github.com/rails/rails/blob/fc35da76e93f8a5d5ace595b4819e19cc0512edd/activerecord/lib/active_record/associations/builder/association.rb#L32

https://github.com/rails/rails/blob/fc35da76e93f8a5d5ace595b4819e19cc0512edd/activerecord/lib/active_record/associations/builder/association.rb#L76

https://github.com/rails/rails/blob/fc35da76e93f8a5d5ace595b4819e19cc0512edd/activerecord/lib/active_record/associations/builder/association.rb#L129

https://github.com/rails/rails/blob/47e3bbeb9057b37c244330cc4e745c8a8090e8c5/activerecord/lib/active_record/associations/has_many_association.rb#L13

上記の順番に追っていけばわかりますが、dependentで指定したオプションを判定して削除処理のbefore_destroyを追加しています。

ということで、dependentはbefore_destroyを追加しているだけなのでdestroyより前に動いちゃうんですね。

じゃあどうする?

下記のソースを見てみるとdependent: :destroyの時にdestroyed_by_associationに値を入れていることがわかります。
https://github.com/rails/rails/blob/47e3bbeb9057b37c244330cc4e745c8a8090e8c5/activerecord/lib/active_record/associations/has_many_association.rb#L27

これを判定に使えそうなので検証したところ下記のようにチェック可能でした。
自前フラグを追加するよりこちらの方がスマートな気がします。

# お店
class Shop < ApplicationRecord
  has_many :books, dependent: :destroy
end

# 本
class Books < ApplicationRecord
  belongs_to :shop

  before_destroy :before_destroy_action

  def before_destroy_action
    # shopを削除した場合は何もしない
    return if destroyed_by_association.present?

    # 最後の1冊の場合は削除できない。
    raise unless Books.not.where(id: id).exists?
  end
end

あとがき

railsのようなオープンソースのフレームワークはソースを簡単にみることができるので仕様を細かく知りたくなったら積極的に読んでみることをお勧めします。
自分でごちゃごちゃやらなくても大抵のことは他の人も困っていて、すでに実装されていたりします。
また、コードリーディングはコーディング力向上に繋がると思いますし、理解が深まればその言語を使うことがより楽しくなると思います。

あとがきのあとがき

ソースを読んでdestroyed_by_associationが使えそうとわかったところで、あらためてググってみたら似たような記事をいくつか見つけました。下記は一例(qiitaにもあった)
https://qiita.com/mishiwata1015/items/ac7c33b5f116111d8568

私はdestroyed_by_associationというキーワードを見つけるまでは関連記事には辿り着けなかったけど、ググり力が高い人はさらっと見つけて時短できるんだろうな。ググり力もエンジニアには重要だなと感じました。

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

カラムの型について

integer:数を扱う
string:文字に使用する。mysqlでは256で転ける仕様になっている?
text:長い文字に使用。ある程度長い文章を取り扱い場合は明示的にするためこちらを利用するほうが吉か。
boolean:真偽
datetime:日付時刻

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

rails のモデルを生成/削除する

rails g model モデル名 で生成 g は generateの略。

rails d model モデル名 で削除 d は destroyの略。

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

何も知らないがActive Jobの基礎 from Rails Guide

Active Jobとは

ジョブを宣言し、それによってバックエンドでさまざまな方法によるキュー操作を実行するためのフレームワーク。

キューとは

first in, first outのデータ構造。スタックの逆。

ジョブとは

定期的なクリーンアップを始めとして、請求書発行やメール配信など、あらゆる処理がジョブになる。これらのジョブをより細かな作業単位に分割して並列実行することもできる。

ジョブとは、コマンドやプログラムがまとまった、ひとかたまりの処理のことだ。

【一通りわかる】Linuxでのジョブの基本的な扱い方まとめ

Active Jobの目的

Railsアプリケーションにジョブ管理インフラを配置すること。

これにより、

  • Delayed JobとResqueなどのように、さまざまなジョブ実行機能のAPIの違いを気にせずにジョブフレームワーク機能やその他のgemを搭載することができるようになる。
  • バックエンドでのキューイング作業では、操作方法以外のことを気にせずに済む。
  • ジョブ管理フレームワークを切り替える際にジョブを書き直さずに済む。

ジョブキュー処理のResqueとDelayed Jobの使い分けの方針などはありますか?

以下の3つはジョブキュー処理の主要gem。

Resque

良い点
- RMagickなどのメモリリークが存在するコードでも不安なくデプロイできる。
- エコシステムが出来上がってる。

悪い点
- 毎回forkするので長時間ジョブ向き。
- キューのストレージとしてRedisが必要。
- メンテが追いついてない。

Delayed Job

良い点
- 特になし。

悪い点
- DMに専用テーブル作成が必要。
- メモリリークしてるコードがあれば定期的な再起動が必要。

Sidekiq

良い点
- Resque互換API。
- 並列に動作するので、外部サイトへのAPI呼び出しなど、I/O待ちの比率が大きいような用途で使うのに便利。
- 1個のプロセスで動作させられるのでメモリ使用量が少なく経済的。

悪い点
- プロセス肥大化には弱い。
- コネクションプールの扱い。
- 並列以外の扱い。

ジョブの作成(飛ばす)

ジェネレータ使ったり。

ジョブの実行

production環境でのジョブのキュー登録と実行では、キューイングのバックエンドを用意しておく必要があります。=> Railsで使うべきサードパーティのキューイングライブラリ(Sidekiq, Resque, Delayed Jobなど)を決める必要があります。
Rails自身が提供するのは、ジョブをメモリに保持するインプロセスのキューイングシステムだけです。プロセスがクラッシュしたりコンピュータをリセットしたりすると、デフォルトの非同期バックエンドの振る舞いによって主要なジョブが失われてしまいます。
アプリケーションが小規模な場合やミッションクリティカルでないジョブであればこれでも構いませんが、多くのproductionでは永続的なバックエンドを選ぶ必要があります。

バックエンド

キューイングバックエンドに接続できるアダプタ(キューイングライブラリ)がビルトインで用意されている。

ジョブはRailsアプリケーションに対して並列で実行されるので、多くのキューイングライブラリでは、ジョブを処理すためにライブラリ固有のキューイングサービスを(Railsアプリケーションの起動とは別に)起動しておくことが求められます。

キュー

多くのアダプタでは複数のキューを扱えます。Active Jobを使って、特定のキューに入っているジョブをスケジューリングできます。

その他

コールバック

Action Mailer

代表的なジョブ。Active JobはAction Mailerと統合されているので、非同期メール送信を簡単に行える。

国際化(i18n)

など

参考リンク

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

【Rails 備忘録】has_many先が存在しない場合、除外して格納する書き方。

はじめに

忘れてしまいがちなので備忘録。

条件

Projectモデルに、has_manyでParticipationモデルが関連付けされている。

createされたprojectにuserが参加アクションをすると、participationが該当のprojectにぶら下がる形でcreateされる。

なので、Projectのインスタンスであるprojectがparticipationを持っていない( @project.participations => [] )ということがありえる。

project.rb
class Project < ApplicationRecord
  has_many :participations



viewでparticipationを持っているprojectだけを表示したい。

やり方として一番簡単そうなのは、例えばcontrollerで
@projects = Project.where(user: current_user)
みたいな感じにして、viewテンプレートで

show.html.erb
<%= @projects.each do |project| %>
  <% if project.participations.present? %>
    <% project.name %>
  <% end %>
<% end %>

↑と書いてしまえばよいが、if文で毎回チェックさせるのもイマイチなので、
 controllerで@projectを格納するタイミングでparticipationがないものを除外させたい。

participationを持っているprojectだけを格納したい

答えを先に書くと、includeswhereを駆使して、

.includes(:participations).where(participations: Participation.all)
みたいな書き方をすればよい。

↓例:

participations_controller.rb
class ParticipationsController < ApplicationController

  def show
    @participation = Participation.find(params[:id])
    @projects = Project.where(client: current_client).where.not(status: "finished").
                  includes(:participations).where(participations: @participations)
  end

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

【Rails 備忘録】has_many先が存在しない場合に除外してインスタンス変数に格納する書き方。

はじめに

忘れてしまいがちなので備忘録。

条件

Projectモデルに、has_manyでParticipationモデルが関連付けされている。

createされたprojectにuserが参加アクションをすると、participationが該当のprojectにぶら下がる形でcreateされる。

なので、Projectのインスタンスであるprojectがparticipationを持っていない( @project.participations => [] )ということがありえる。

project.rb
class Project < ApplicationRecord
  has_many :participations



viewでparticipationを持っているprojectだけを表示したい。

やり方として一番簡単そうなのは、例えばcontrollerで
@projects = Project.where(user: current_user)
みたいな感じにして、viewテンプレートで

show.html.erb
<%= @projects.each do |project| %>
  <% if project.participations.present? %>
    <% project.name %>
  <% end %>
<% end %>

↑と書いてしまえばよいが、if文で毎回チェックさせるのもイマイチなので、
 controllerで@projectを格納するタイミングでparticipationがないものを除外させたい。

participationを持っているprojectだけを格納したい

答えを先に書くと、includeswhereを駆使して、

.includes(:participations).where(participations: Participation.all)
みたいな書き方をすればよい。

↓例:

participations_controller.rb
class ParticipationsController < ApplicationController

  def show
    @participation = Participation.find(params[:id])
    @participations = Participation.search_by_client(current_client)
    @projects = Project.where(client: current_client).where.not(status: "finished").
                  includes(:participations).where(participations: @participations)
  end

show.html.erb
<%= @projects.each do |project| %>
  <% project.name %>
<% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6 のちょい足しな新機能を試す42(multi-db db:migrate:status編)

はじめに

Rails 6 に追加されそうな新機能を試す第42段。 今回は、 multi-db db:migrate:status 編です。
Rails 6 では、 複数データベースに対応しているため、 db:migrate:status も複数データベースに対応しています。

Ruby 2.6.3, Rails 6.0.0.rc1 で確認しました。Rails 6.0.0.rc1 は gem install rails --prerelease でインストールできます。

$ rails --version
Rails 6.0.0.rc1

今回の準備

今回は、 Rails6 のちょい足しな新機能を試す35(複数データベース migration --database オプション編) をベースとして進めます。第35段まで終わっている状態を前提として話しを進めます。

一旦DBを作り直す

必要ないかも知れませんが、念のため、綺麗な状態から作業したいため、DBを作り直すことにしました。

bin/rails db:drop db:create db:migrate

カラムを追加するマイグレーションを作成する

これもまあ、必要ないと言えば必要ない気がするのですが、 backboneusers テーブル に email カラムを、
librarybooks テーブルに isbn カラムを追加するマイグレーションを作成します。

library 側に追加するためには、 --database (--db) オプションが必要です。

$ bin/rails g migration AddEmailToUser email
$ bin/rails g migration AddIsbnToBook isbn --database=library

db:migrate:status で確認する

db:migrate:status で確認すると、両方のデータベースのマイグレーションの状況が確認できます。

$ bin/rails db:migrate:status

database: backbone_development

 Status   Migration ID    Migration Name
 --------------------------------------------------
    up     20190608225735  Create user
   down    20190622004922  Add email to user


database: library_development

 Status   Migration ID    Migration Name
 --------------------------------------------------
    up     20190608225808  Create book
   down    20190622005021  Add isbn to book

どちらか1つのデータベースのマイグレーションの状態を知りたい場合は、 status に続けて :backbone:library を指定します。

$ bin/rails db:migrate:status:backbone

database: backbone_development

 Status   Migration ID    Migration Name
 --------------------------------------------------
    up     20190608225735  Create user
   down    20190622004922  Add email to user

$ bin/rails db:migrate:status:library

database: library_development

 Status   Migration ID    Migration Name
 --------------------------------------------------
    up     20190608225808  Create book
   down    20190622005021  Add isbn to book

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try042_multidb_migrate_status

注意

上のブランチで試す場合は、以下のように実行すれば、 bin/rails db:migrate:status の結果が同じになります。

$ bin/rails db:drop db:create
$ bin/rails db:migrate:backbone VERSION=20190608225735
$ bin/rails db:migrate:library VERSION=20190608225808

参考情報

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

【Rails】ER図 と アソシエーションを100%理解する

概要

モデル同士の繋がりを指すアソシエーション(関連付け):paperclips:
定義しておくとモデルをまたいだデータの呼び出しが可能になります。

また親と子の関係や、外部キーの設定などを理解していないと、
「親モデルのデータを先に追加しないと子のデータが追加できない。」なんて
DB設計の見直しが起きる可能性も・・:fearful:

データベースを可視化できるER図はとても大切。学習メモとして残します:blossom::sunny:

参考

(1)感動の分かりやすさ。ER図の基礎の基礎知識もあり。初心者必見 :sunrise:
【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】

(2) 理解に苦しんだ・・ 中間テーブルってやつです。
【初心者・独学者向け】Ruby on Railsで中間テーブルを作成し、多対多を実現する

親と子の関係

【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】 がとても分かりやすかったので引用します:pushpin:

モデル同士の親子関係とは
図で考えてみましょう。
Aさん(User)は、自分が作ったブログサイトでたくさんの記事(Article)を投稿します。

qiita5.png

Bさんも少しは投稿します。

qiita6.png

つまり、User一人一人は沢山のArticleを持っている(User has many articles.)、と考えることができます。(この突然出てきた英文は伏線ですよ!!!!)

qiita6.5.png

逆の立場(Article)も考えてみましょう。
ある日投稿された記事(Article)は、Aさん(User)によって書かれました。

qiita7.png

次の日投稿された記事はBさんによって書かれました。
他の日に投稿された記事も、それぞれAさん、Bさんどちらかによって書かれた記事です。Aさん、Bさんが共作で書いた記事というのはありえません。

つまり、Articleは誰か一人のUserに所属している(Article belongs to a user.)と考えることができます。

qiita7.5.png

UserがいなくてはArticleは生まれないし、Articleは必ず誰か一人のUserから生まれます。そう、Userが親でArticleが子となっているわけなのです。これが親子関係です。

このような関係をUserとArticleは一対多の関係または1:Nの関係といいます。
もちろんUserが1でArticleが多です。

この親と子の関係をつくるためのデータベース上でのモデルの関連付けをアソシエーションといい
アソシエーションしないと、どのユーザーがどの記事が書いたのか分かりません。

多対多の関係と中間テーブル

多対多の関係とは ?

どちらのモデルから見ても、自分も相手も複数×複数になる状態です。

記事から見るとカテゴリーを複数持っているので、記事に対して、カテゴリーは多になります。

カテゴリーから見ると1カテゴリーに複数の記事が関連しているので、カテゴリーから見ると記事は多になります。 この>関係が多対多になります。

【初心者・独学者向け】Ruby on Railsで中間テーブルを作成し、多対多を実現する

中間テーブルとは?

多対多の関係は2つのモデルでは実現できないんです。
え、なぜ?:disappointed_relieved:

例えば、先ほどの記事とカテゴリーの例。

【記事テーブル】

id 記事タイトル

記事ができる度に、自動生成のidが付いていきます。
主キー(primary key)ですね。

【カテゴリーテーブル】

id カテゴリー名
日常
Rails

【中間テーブル】

id 記事タイトル カテゴリー名

可能性の話ですが、上記のように 1つの記事がもしかしたら何個もカテゴリーを持つかもしれないわけです。
「記事タイトル」: ライフスタイル / アウトドア / 子育て とかそんな感じでしょうか。

今はまだ記事タイトルもカテゴリーもどちらも2つしかないので、
中間テーブルいらなくない?ってなると思います。でも記事とカテゴリーはどちらもどんどんどんどん増えていく可能性がある・・

そうするとこの組み合わせは無限大になるし、
カラム数は最初に設定したらもう増やせないので、多対多の間は中間テーブルで対応してあげます。

モデルの関連性設定に has_many, through:が加わります!

上の例で引き続き。
中間テーブルができるとモデルの関連性設定が変わります :fist_tone4:

まずはマイグレーションを実行。

20190627075545_create_categories_articles.rb
class CreateCategoriesArticles < ActiveRecord::Migration
  def change
    create_table :categories_articles do |t|
      t.integer  :category_id
      t.integer  :article_id
      t.timestamps null: false
    end
  end
end

次に中間テーブルで記事モデルとカテゴリーモデルとの関連付けを行う。

models/categories_ariticles.rb
class CategoriesArticle < ActiveRecord::Base
  belongs_to :category
  belongs_to :article
end

記事モデルの関連付け :paperclips:

models/article.rb
class Article < ActiveRecord::Base

  has_many :categories_articles
  has_many :categories, through: :articles_categories

end

カテゴリーモデルの関連付け :paperclips:

models/category.rb
class Category < ActiveRecord::Base
  has_many :categories_articles
  has_many :articles, through: :categories_articles
end

中間テーブルを通して繋がっているものには、
through:: というkeyをつけます。

throughオプションによりarticles経由でcategoryにアクセスできるようになります:lemon:

外部キーとは? (foreign key)

リレーショナルデータベース(RDB)で、テーブルのある列に、別のテーブルの特定の列に含まれる項目しか入力できないようにする制約。

商品が消えたら、いいねも消える

例えば、あるECサイト
ユーザーのマイページには自分がいいねした 「いいね一覧」 があるとします。
個人的には商品がもう存在しない場合には、いいねも消えてしまっていいのでは?と思います。

そんな時はdependent: :destroyを使います:fist_tone3:

dependent: :destroy の役割

商品といいねのモデルには、以下のようなリレーションの記載があります。

models/product.rb
class Product < ApplicationRecord

has_many :favorites, dependent: :destroy

end
models/favorite.rb
class Favorite < ApplicationRecord
    belongs_to :user
    belongs_to :product
end

dependent: :destroyと言う箇所は、親にあたる投稿が削除されたら、
子をどう扱うのかオプションを設定できるという感じです。今回は消去するので:destroy
残しておいて良いのであれば、何も記載しなくてOkay

【Rails】ActiveRecordの:dependent使い分けまとめ【: destroy, :delete, :nullify】

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

【Rails】Action Mailer でメール送信機能をつくる

概要

Action Mailerを使ってメールの送信機能をつくります:speech_balloon:
今回は管理者がユーザーからのお問い合わせに対して、管理者画面から返事をすると、
ユーザーにメールで送信される機能を実装していきます。

学習メモとして記録 :writing_hand_tone4:

参考

(1)RAILS GUIDES: Action Mailer の基礎
(2)【Ruby on Rails】メール送信の実装手順(ActionMailer)とはまったエラーなど
(3)Railsでメール自動配信機能をつくるまでの道程
(4)Rails の ActionMailer でメール送信処理
(5)RailsのAction Mailerでメール送信

Action Mailerとは

Ruby on Rails に組み込まれているメール送信機能のこと。
Action Mailer を使うと、Ruby on Rails からメールを送信してくれます :envelope:

・メールマガジンの一斉送信
・ウェブサイトに会員登録した時のthank youメール
・お問い合わせフォームの記入内容が管理者にメールでも送信される

のような時にアプリケーションからメールを送信する機能と解釈してます。
アプリケーションのメーラークラスやビューからメールを送信することができる便利な機能です!

導入手順

1. メイラーを生成

railsコマンド(rails generate)で生成します。 ContactMailer は任意クラス名。
今回はお問い合わせ関係のメール送信機能を作成したいためContactMailerです。

terminal.生成
$ rails generate mailer ContactMailer

terminal.結果
Running via Spring preloader in process 1893
   create  app/mailers/contact_mailer.rb
   invoke  erb
   create    app/views/contact_mailer
   invoke  rspec
   create    spec/mailers/contact_mailer_spec.rb
   create    spec/mailers/previews/contact_mailer_preview.rb

無事に生成されましたね :v_tone3:

2. サーバーを設定

メールを送信するときは、送信するサーバーが必要。と言うことで、
config/environments/development.rbにメール送信設定を記述します。
今回はg-mailを使う記載方法です。

development.rb
Rails.application.configure do
  #--- 中略 ---#
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    port:                 587,
    address:              'smtp.gmail.com',
    domain:               'gmail.com',
    user_name:            '<YOUR EMAIL ADDRESS>',
    password:             '<YOUR EMAIL PASSWORD>',
    authentication:       'login',
    enable_starttls_auto: true
  }
end

port:も上記のままで大丈夫です。

上から順々に見ていきます。
ここでは、config.action_mailerというパラメーターに色んなオプションを指定してます。

1行目 raise_delivery_errors
メールの送信に失敗した時にエラーを出すかどうか (出したいので true)

2行目 delivery_method
メールの送信方法。 デフォルトで :smtd なので気にする必要もないのですが、
わたしみたいに「なにそれ!?」ってなった方は以下の引用を読んでみてください。

「SMTP」とは「Simple Mail Transfer Protocol(シンプル・メール・トランスファー・プロトコル)」の略で、あえて>訳せば「簡単なメールの送信の手順」というところだろうか。お約束ごとと考えてもいい。

あなたがメールを書き、宛先のアドレスを入力して「送信」アイコンをクリックする。このとき、あなたのスマホやパソコン>は、この「SMTP」のお約束ごとに従って、あなたが契約しているメールサーバーと、こんなやり取りをするのである。
「メールを送るよ〜」「ええで!」「宛先は〇◯だよ」「りょ」「本文はかくかくしかじかだよ」「受け取ったで!」――とまぁそんな具合。

出典: メール設定で最初につまずく「SMTP」「POP」「IMAP」。その意味&設定方法は?

3行目 smtp_settings
smtpの詳細設定って感じです。

  1. port => SMTPサーバーのポート番号
  2. address => SMTPサーバーのホスト名
  3. domain => HELOドメイン
  4. user_name => メール送信に使用するgmailのアカウント
  5. password => メール送信に使用するgmailのパスワード
  6. authentication => 認証方法
  7. enable_starttls_auto => メールの送信にTLS認証を使用するか

3. メーラーを編集

メーラーってRailsのコントローラーと似てるんですね。
「アクション」と呼ばれるメソッドがあり、メールの内容をつくるのにビューを使います。

生成直後は、以下のような application_mailer.rb

app/mailer/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: 'from@example.com'
  layout 'mailer'
end

空のメーラー

app/mailer/contact_mailer.rb
class ContactMailer < ApplicationMailer
end

があるはずです。

application_mailerには全メーラー共通の設定を、
sample_mailerにはメーラー個別の設定をします。

application_mailerを編集します

app/mailer/contact_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from:    "管理人 <from@example.com>",
  layout 'mailer'
end

共通の処理・設定を記述する場合にはdefaultメソッドを使用します。

プロパティ 役割
to 送信先の指定
cc 一斉送信先の指定
bcc 非表示送信先の指定
from メールの送信元名
subject メールタイトル
date メールの送信日時
reply_to 返信用アドレスの指定

などが指定できます。

contact_mailer.rb を編集します

今回は管理者の返信がユーザーのe-mailに届くようにします。
なのでメソッドをsend_when_admin_replyと定義しました。

app/mailer/contact_mailer.rb
class ContactMailer < ApplicationMailer

  def send_when_admin_reply(user, contact) #メソッドに対して引数を設定
    @user = user #ユーザー情報
    @answer = contact.reply_text #返信内容
    mail to: user.email, subject: '【サイト名】 お問い合わせありがとうございます'
  end

end

個別の設定にはmailメソッドを使用します。
send_when_replyedメソッドを呼び出す時に渡されるユーザーの情報から、
emailアドレスだけを取り出してメールの送信先としてします。

mailメソッドが呼び出されると、メール本文が記載されているビューが読み込まれます。
インスタンス変数(@xxx)でメーラービューに値を渡してあげたいので、インスタンス変数を用意してる感じです。

次は、そのメールの本文を作成していきます :keyboard:

4. メールの本文を作成する(メーラービューの作成)

app/views/contact_mailerディレクトリ下にファイルを2つ作成します。

1つはHTMLフォーマット、もう一つはテキストメール。
顧客によってはHTMLフォーマットのメールを受け取ることができない / 受け取りたくない人もいるので、テキストメールも作成しておくのが最善です。

HTMLファイルの作成

views/contact_mailer/send_when_admin_reply.html.erb
  <h2><%= @user.name %> 様</h2>
 <p>この度は、お問い合わせありがとうございました。<br>
 以下でご質問の回答となっておりますでしょうか。</p>

  <p><%= @reply %></p>

  <p>今後とも XXX をよろしくお願いいたします。</p>

テキストファイルの作成

views/contact_mailer/send_when_admin_replyd.text.erb
  ===============================
  <%= @user.name %> 様
  ===============================

  この度は、お問い合わせありがとうございました。
 以下でご質問の回答となっておりますでしょうか。

  <%= @reply %>

  今後とも XXX をよろしくお願いいたします。

5. 実際に処理を走らせるアクションにメール送信処理をさせる

メールの設定を記述しただけではメールは送信できません。
メーラー(ここではapplication_mailer.rb / contact_mailer.rb)は各コントローラーのアクションからの呼び出しによって起動します。

管理者がアプリケーション管理画面フォームより返信を送信し、その内容をメールでユーザーに送信します。
実際に処理を走らせるアクションはadmin/contacts_controller.rbとなります。

app/controllers/contacts_controller.rb
class Admin::ContactsController < Admin::ApplicationController
  def update
   contact = Contact.find(params[:id]) #contact_mailer.rbの引数を指定
   contact.update(contact_params)
   user = contact.user
   ContactMailer.send_when_admin_reply(user, contact).deliver
  end
end

例えば、ユーザーが新規アカウント登録をしてメールを送信する場合は、

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user

  def create
    if @user.save #ユーザーのインスタンスが新しく生成されて保存されたら
      NotificationMailer.send_when_signup(@user).deliver #確認メールを送信
      redirect_to @user
    else
      render 'new'
    end
  end
end

createアクションに処理を走らせることになります。

わたしのアプリケーションでは、DBの設計上
ユーザーが問い合わせをした時点で、Contactsテーブルの中に、
1つレコードが準備されていて、その中に'返信'というカラムが用意されています。

なので、updateアクションを使って
すでにあるレコードの中に管理者の返信のデータだけを更新してあげる形になります:sunflower:

6. テストしてみる

以上でメーラーができるはずです・・!
実際にアプリケーションを動かしてみて、メールを受信できたら成功 :ok_hand_tone3:

以上、学習メモでした :hatched_chick::writing_hand_tone4:

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

RailsでFontAwesomeを使う

内容

  • FontAwesomeとは
  • FontAwesomeを使う準備
  • FontAwesomeをhamlで
  • FontAwesomeをボタンとして使う(最終目標)
    • link
    • file選択

初学者の復習も兼ねた投稿ですので、間違い等ありましたらご指摘いただけると嬉しいです。

FontAwesomeとは

FontAwesome

こちらが公式ページです。
アイコンを自由に使えます!一気にサイトがいい感じになります。

FontAwesomeを使う準備

導入方法はいくつかありますが、今回はRailsで使うのでgemをインストールします。

Gemfile
gem 'font-awesome-sass', '~> 5.4.1'
terminal
bundle install
/assets/stylesheets/application.scss
@import 'font-awesome-sprockets';
@import 'font-awesome';

これで準備完了です。

FontAwesomeをhamlで

= fa_icon 'edit'

これでOKです。

'edit'の部分は、FontAwesomeのページから使いたいアイコンを探して、
そのアイコン名を入れます。

デベロッパーツールで確認してみると、

<i class="fa fa-edit"></i>

こんな感じで反映してくれてます。

FontAwesomeをボタンとして使う

link_toで使う

スクリーンショット 2019-06-26 22.11.23.png
こんなイメージです。

ペンのアイコンが新規作成ページへのリンク、
歯車のアイコンが編集ページへのリンクになってます。

通常のlink_toは

= link_to "New", root_path

のような形で第一引数にリンクの名前、第二引数に行き先を書くと思いますが、
doを使ってブロックにしてあげることでFontAwesomeが使えます。

= link_to new_path do
  = fa_icon 'edit'
= link_to edit_path(current_user) do
  = fa_icon 'cog'

(_pathの部分は仮です)

インデントにご注意!
(そもそもhamlにはdoに対するendが不要ということを知らずにしばらく悩んでしまいました…)

先ほど示したように
<i class="fa fa-edit"></i>
このような形になるので、

i {
  font-size: 1.3em;
  color: black;
    }

こんな感じでcssを書いてあげるとスタイルを変更できます。

file選択のボタンとして使う

スクリーンショット 2019-06-26 22.17.45.png
フォームの端にある写真アイコンを押すと、ファイル選択の画面が開くようになってます。

デフォルトの画像選択ボタンを変更

編集の都合上、ちがうサイトを参照します。分かりにくくてすみません。

スクリーンショット 2019-06-27 9.38.45.png
元々はこういう「ファイルを選択」ボタンになっていると思います。
これをdisplay:none;で見えなくしちゃいます。

で、labelタグをつけて、そこにスタイルをあててあげます。

haml
.field
  = f.label :image, "ここを押したら画像選択できるよ", class: "label"
  = f.file_field :image, class: "file"
scss
.label {
  cursor: pointer;
  font-size: 16px;
  border: 1px solid gray;
  border-radius: 10px;
  padding: 10px;
}

.file {
  display: none;
} 

スクリーンショット 2019-06-27 9.51.45.png
こんな感じになりました。

参考:https://proengineer.internous.co.jp/content/columnfeature/7605

FontAwesomeに置き換え

ここまで来たらあとは簡単です。
先ほどラベルタグをつけたところを do でブロックにしてFontAwesomeにするだけです。
(くれぐれもインデントにご注意!(二度目))

haml
= form_for [@group, @message] do |f|
  = f.label :image, class: "main-form__image" do
    = fa_icon "image"
    = f.file_field :image, class: "main-form__image--default"
scss
.main-form__image {
  cursor: pointer;
  i {
    font-size: 1.3em;
    color: black;
  }
  &--default {
    display: none;
  }
}

これでこんなボタンを作ることができました!
スクリーンショット 2019-06-26 22.17.45.png

余談

個人的にこの実装に結構悩んで時間を使ってしまったのですが、
なぜかというと、やりたい実装を分解して考えられなかったからです。

検索するときに
「FontAwesomee file選択」
など、いきなりFontAwesomeをfile選択ボタンにするやり方を調べてしまっていました。

が、分解して考えると、
1. デフォルトの選択ボタンを変更する
2. 変更したものをFontAwesomeに置き換える
という手順を踏むことになります。

なので検索も
「file選択 変更」
などにすれば一気に答えに近づきます。

まだプログラミング学習一ヶ月ですが、
エンジニア力はググり力、というのを実感しました。

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

【Rails】factory_botの使い方について

はじめに

Railsにはfactory_botという便利なgemがあり、そのgemについてまとめました。
factory_botというgemを一言で言うとRspecで用いるテストデータの作成を楽にしてくれるgemです。
Rspecを入れていない方は下記の記事を参考にして入れると良いと思います。

@Mosacさんの記事
RSpecをRailsにインストールする

factory_botを使用する場合は下記のような形でGemfileに記述します。

Gemfile
gem "factory_bot_rails"

使用方法

導入方法

spec/配下にsupportフォルダを作成し、support配下にfactory_bot.rbを作成し、以下を記述する。

factory_bot.rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

spec/rails_helper.rbにコメントアウトしてある下記のコメントアウトを外し、
support配下のファイルを読み込むように設定する。

rails_helper.rb
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

テストデータの作成方法

spec/factories/配下にテストしたい該当モデルのファイルを作成する。
Userモデルならusers.rbという形でファイルを作成する。
カラムに仮の値を定義して作成する。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "hoge" }
    email { "foo@bar.com" }
  end
end

上記の場合、userとしてnameがhoge,emailがfoo@bar.comのデータが仮作成される。

モデル名以外の名前をつける場合は下記のように記述する。

spec/factories/users.rb
FactoryBot.define do
  factory :adminuser, class: User do
    name { "hoge" }
    email { "foo@bar.com" }
  end
end

Rspecでのテストデータ使用方法

定義の仕方は下記の通り。

spec/models/users_spec.rb
RSpec.describe User, type: :model do
  let(:user){ FactoryBot.create(:user) } #通常の書き方 DB登録される
  let(:user){ create :user} #省略記法

  let(:user){ FactoryBot.build (:user)} #通常の書き方 DB登録されない
  let(:user){ build :user} #省略記法

  let(:user){ build :user, name: "hogehoge"} #上書きすることも可能
end

上記のようにデータを定義することによって、モデルやコントローラー上で
仮のデータを使ったテストをすることが可能になる。

参考記事

【Rails】factory_botの使い方メモ
RailsアプリへのRspecとFactory_botの導入手順

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

gem impressionist を使ってユニークpv数を計測【Rails】

ruby 2.5.1
rails 5.2.1

はじめに

impressionistを使ってpv数を計測しようと思い、色々調べながら実装するもなかなかユニークpv数が取得できずに苦戦していました。結果的にreadme(https://github.com/charlotte-ruby/impressionist) を見て解決したので、その記録として残しておきます。最初からreadme見ればよかった。。。

導入方法

Gemfile
gem 'impressionist'
$ bundle install

次にpvを保存するテーブルを作成します。

$ rails g impressionist

このようになっていれば大丈夫です。

Running via Spring preloader in process 40810
      invoke  active_record
      create    db/migrate/20190626153731_create_impressions_table.rb
      create  config/initializers/impression.rb

migrateします。

$ rails db:migrate

Model

今回はQ&Aサイトの質問ページのpv数を取得したいためQestionモデルに以下のように記載します。

question.rb
class Question < ApplicationRecord
  is_impressionable
end

Controllers

セッションハッシュでフィルタリングされたモデルからユニークpv数を取得します。IPで判別すると同じIPを使用する訪問者を計測できないのでセッションハッシュで判別するのがいいっぽいです。

question_controller.rb
class QuestionsController < ApplicationController
  impressionist unique: [:session_hash]

  def show
    @question = Question.find(params[:id])
    impressionist(@question, nil, unique: [:session_hash])
  end
end

Views

show.html.erb
<%= @question.impressionist_count %>

スクリーンショット 2019-06-27 2.08.00.png

更新してもテーブルのレコードや表示されるpv数が増えていなければ完璧!

他の記事を参考したけど全然できなかったのでreadmeを読んだのですが、これが足りなかったようです。
impressionist unique: [:session_hash]

公式リファレンスを見る習慣をつけます。

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

Ruby on Rails

Ruby on Rails

  • Ruby on Railsの記事編集中
abc abc abafdafd
afdfdsafdsaf dfdsfsdfa fdafdafa
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む