20190827のRubyに関する記事は19件です。

Rails6でブログのタグのacts-as-taggable-onがエラーになったので別のタグgem(gutentag tags)を使う

Rails6にアップデートした時の問題

Rails6になってから、今まで使っていた acts-as-taggable-on がエラーになるようになった。

原因は

まあ悪い意味での原因というわけではなく

https://github.com/rails/rails/pull/35933

この部分で set_attribute_was が消されたため発生したもの

また、これの対応のブランチもあるのだが、初歩的なバグを含むため、ブランチ切り替えの対応してもあまり意味がない。

とはいえ、ライブラリへのPRも全然反映されないし、Gemfileが汚れるのも嫌なので別のやつを実験的に使ってみた。

https://github.com/pat/gutentag

というやつで、スターは315の、RubyGemダウンロード数は54,474というところなので、act-as-taggable-onのわずか6%程度のスター数となり、多少心もとない。

開発当初のブログがあるので、読んでみた

個人的には、act-as-taggable-onしか使ったことなかったので、とりあえず調べていたら、どうも2013年頃に開発されたようだ。

https://freelancing-gods.com/2013/07/11/gutentag-simple-rails-tagging.html

https://github.com/pat/gutentag/blob/master/lib/gutentag/persistence.rb

永続化周りをforwardableを使って委譲して実装してある

使ってみた

環境

  • Rails 6.0.0
  • ruby 2.6.3

Gemfile

記載したら、bundle install する

gem 'gutentag', '~> 2.5'

インストール設定

ここは、よくあるお決まりのやつをただ打っていくだけ
migrateすることで、gutentagにより作られたマイグレーションファイルが実行され、使えるようになる
使う側のモデルにカラムを追加することなく使えるところが楽で良い

bundle exec rake gutentag:install:migrations
rails db:migrate

Schema例

create_table "blogs", force: :cascade do |t|
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

Blog.rb

今回、ブログにタグ機能をもたせたいので、
ここにGutentag::ActiveRecord.call selfを置く

class Blog < ApplicationRecord
  Gutentag::ActiveRecord.call self
  include TagExtensions
end

FactoryBot.define do
  factory :blog do
    tags_as_string { "hoge,fuga,piyo" }
  end
end

TagExtensions.rb

これは、いわゆるattr_accessorの役割をincludeして使うために別途作っただけ
メソッド毎モデルに直で書いても良いが、他のモデルでも使いたかったのでこのようにサクッと作っただけ。(若干気に入らないので後で修正する気がする)

module TagExtensions
  def tags_as_string
    tag_names.join(', ')
  end

  def tags_as_string=(string)
    self.tag_names = string.split(/,\s*/)
  end
end

作成時

View

一般的なinputタグに先程TagExtensionsで作ったgetterメソッドを配置しておく

  .form
    = f.label :tags_as_string
    = f.text_field :tags_as_string, value: @blog.tags_as_string

Controller#create

Scaffoldして、必要なところだけ抜粋しておく
フォームから送られて新規作成するところの処理と、Strong Parametersの設置

class BlogsController < ApplicationController
  def create
    @blog = Blog.new(blog_params)

    respond_to do |format|
      if @blog.save
        format.html { redirect_to blog_url(@blog), notice: 'Blog was successfully created.' }
        format.json { render :show, status: :created, location: @blog }
      else
        format.html { render :new }
        format.json { render json: @blog.errors, status: :unprocessable_entity }
      end
    end
  end

  def blog_params
    params.require(:blog).permit(:tags_as_string)
  end
end

一覧表示時

View

tag_namesなどで引っ張ってこれる

- @blogs.each do |blog|
  - blog.tag_names.each do |tag|
    = tag

Controller#index

例えばincludesなどをしないとN+1で死ぬので注意

class BlogsController < ApplicationController
  def index
    @blogs = Blog.includes(:tags)
  end
end

挙動確認

実際にテストなどで挙動見てみるとこんなように使える

blog = Blog.create! # ブログ用レコード作る

blog.tag_names << 'hogehoge'
blog.tag_names #=> ['hoge']
blog.tag_names << 'fuga' << 'piyo'
blog.tag_names #=> ['hoge', 'fuga', 'piyo']
blog.tag_names -= ['fuga']
blog.tag_names #=> ['hoge', 'piyo']

blog.save # 保存する

感想

gutentag自体は、rails6にすぐ対応したcommitがあるし、issueのclose率も100%なのでけっこういいかなと思った。懸念点はpatという創造者しかほぼ保守していないところ。
acts-as-taggable-onは、使ってる人が多いので、また修正版が上がりそうなので、そのあたり様子見

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

HerokuのDB更新時のActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR: column ... does not existの対処方

PG::UndefinedColumn: ERROR

ローカル環境に置けるrails db:migrateはエラーが起きなかったが、
本番環境(Heroku)に heroku run rails db:migrateをすると出るエラー

今回起きた要因

これが発生した要因として
今までは普通にできたが、不要なカラができてしまったので削除する。
そしてHerokuも同様に反映させようとしたが今回のエラーが発生。
ゆえに今回はカラム削除のDBファイルがからんでいると推測。

エラー内容 &対処法

PG::UndefinedColumn: ERROR: column "string" for relation "users" does not exist

とでていた。
そこでusersテーブルの削除したファイルを見てみると

..._remove_delete_to_users
 class RemoveDelete2ToUsers < ActiveRecord::Migration[5.1] 
   def change 
     remove_column :users, :job, :string 
     remove_column :users, :string, :string 
     remove_column :users, :basic_overtime_pay, :string 
     remove_column :users, :integer, :string 
     remove_column :users, :uid, :string 
     remove_column :users, :string, :string 
     remove_column :users, :image, :string 
     remove_column :users, :string, :string 
     remove_column :users, :encrypted_password, :string 
     remove_column :users, :string, :string 
     remove_column :users, :reset_password_token, :string 
     remove_column :users, :string, :string 
     remove_column :users, :note, :string 
     remove_column :users, :string, :string 
   end 
 end 

このようにカラムの型がたくさんある状態になっている。
そこでここを整理整頓してみる。

..._remove_delete_to_users
 class RemoveDelete2ToUsers < ActiveRecord::Migration[5.1] 
   def change 
     remove_column :users, :job, :string 
     remove_column :users, :basic_overtime_pay, :integer 
     remove_column :users, :uid, :string 
     remove_column :users, :image, :string
     remove_column :users, :encrypted_password, :string 
     remove_column :users, :reset_password_token, :string
     remove_column :users, :note, :string 
   end 
 end 

ここで改めて heroku run rails db:migrateをすると
通った!!!
ロールバックするさいもカラムと型が合わずにうまくいかない!
みたいなことがあるみたいなのです。
今回のように同じエラーがでてるかたは参考にしてみていただけると幸いです。

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

rails viewの繰り返しでitemの一覧を表示する方法

railsのviewでアイテムをfor文を使って表示しています。

<%for i in 0..4 do%>
    <li><%=link_to @ranking_articles[i].title,article_path(@ranking_articles[i].id)%><span>(<%=@previews[i]%>)</span></li>
<%end%>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsの勉強会に行ってきた話

こんにちは。はじめての投稿になるので、よろしくお願いします。

感想

とても楽しい時間を過ごすことができました。
技術的な話としては、MVC構造は1960年代?ごろから使われていたものらしく、結構古いもののようで、それをRailsは未だに使っていて大丈夫なの?っていうのが話題にでたときに、Railsが批判されてる理由が少しわかった気がしました。

自由時間に現役のエンジニアの方からアドバイスをもらうことができて、とても参考になりました。きつい時もあるけど、楽しくコードを書いていこうというのが励みになりました。勉強会の後に、数人で食事に行き、業界のことなどいろいろ話ができて楽しかったです。ちなみに奢ってもらっちゃいました笑

Ruby界隈は、親切な人が多いなと思いましたし、いろんな所でイベントがあり、活発なのでまた行ってみたいと思いました。

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

プログラミング学習(三日目)ProgateでRuby On Rails レッスン3

今日学んだこと

find_byメソッドで、データベースからデータを取得

モデル名.find_by(カラム名:値)で、指定したカラムの値を持つレコードを取得できる
例えば、
>a = Post.find_by(id:3)
で、変数aにpostsテーブルのidが3のレコードを代入できる。
そしてここから、
>a.カラム名
とすれば、idが3の指定したカラム名の値を出力できる。

ルーティングのURLに:idで、任意のすべての値を適用

具体的に言うと、

routes.rb
get "/home/:id" => "home#about"

とすると、URLが「/home/a」でも、「/home/xyz」でも、「/home/1234567890」でも、
homeコントローラのaboutアクションを指定されると言う訳。
ちなみに、ルーティングはファイルの上から順に該当するURLを探すので、:idを使う場合は下の方にやらないとやばい。

paramsの使い方

そして上のURLの:idに該当する値は、コントローラ内のアクション内で取得できる。
paramsにハッシュで入っているらしく、params[:id]とやると取得できる。

入力フォームを作成し、データベースに保存する方法。

これが少し複雑で難しかった。
「form_tag」と「redirect_to」と「post」を使う

まずhtml.erbファイルに入力フォームを作るのだが、
<%= form_tag("送信先のURL") do %>
<textarea name="test"></textarea>
<input type="submit" value="送信">
<% end %>

と作る。form_tagで送信先のURLを設定しつつ、入力内容の属性をname属性とし{test:"入力した内容"}というハッシュで、指定したURLに対応するコントローラのアクションへと送られる。
ちなみに、textareaにname属性を付与しないと送信できないらしい。

そしてそのアクションへ入力内容が送られるわけだが、まずそこでここではpostsテーブルを用いているので、Postインスタンスを作成し保存する。その際にparamsを使い、任意のカラムが入力された内容になるインスタンスを作成する。
こんな感じ(postsテーブルにはcontentカラムがある)
**def create**
** @inputfile = Post.new(content:params[:test])**
** @inputfile.save**
** redirect_to("送信先のURL")**
**end**

ちなみに「redirect_to」とは、アクションに対応するビューファイルが無い時にエラーにならないために、処理?データ?をほかのアクションへ送るメソッドである。

データの並べ替え

orderメソッドを用いて、「order(カラム名:並び順)」と指定するとその指定した並び順になる。昇順が「:asc」、降順が「:desc」
@posts=Post.new.order(id: :asc)とすると、idが1から並ぶ。

結構抽象的に書いた分、早く書けたが少しわかりにくい可能性が、、、

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

#Ruby #Rails で 日付同士の差分が 何週間ぐらいかを 計算しようとしてみた記録 ( ActionView::Helpers::DateHelper が使えない ))

Ruby #Rails record to calculate how many weeks the difference between dates is (ActionView :: Helpers :: DateHelper cannot be used))

ActionView::Helpers::DateHelper が週に対応してないようなので

require 'action_view'
require 'action_view/helpers'

include ActionView::Helpers::DateHelper

distance = if some_date.between?(1.weeks.after, 1.month.after)
             distance_weeks = (some_date - Date.current).to_i / 7
             "#{distance_weeks}週間"
            else
              distance_of_time_in_words(Time.zone.now, some_date)
            end

https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2356

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

RSpecの `contain_exactly` 相当でハッシュを検証する方法に悩んでしまった話

ハッシュの { a: 1, b: 2 }{ b: 2, a: 1 } が、順序を無視して完全に一致するかテストしたかった。

contain_exactly

RSpecには順序を無視してくれる contain_exactly があるが、これは配列に対して使うものらしい。
https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/contain-exactly-matcher

The contain_exactly matcher provides a way to test arrays against each other in a way that disregards differences in the ordering between the actual and expected array.

ハッシュでやってみると実際おかしなことになって失敗する。

     Failure/Error: expect({ a: 1, b: 2 }).to contain_exactly({ b: 2, a: 1 })

       expected collection contained:  [{:a=>1, :b=>2}]
       actual collection contained:    [[:a, 1], [:b, 2]]
       the missing elements were:      [{:a=>1, :b=>2}]
       the extra elements were:        [[:a, 1], [:b, 2]]

エラーメッセージから考えると、一応 contain_exactly 側のハッシュを * で展開すればテストが通る。

require 'rspec/core'

RSpec.describe 'Hash' do
    it 'contains exactly' do
        expect({ a: 1, b: 2 }).to contain_exactly(*{ b: 2, a: 1 })
    end
end

しかしもっと単純な方法があった。

結論

普通に eq で検証すればいい。

require 'rspec/core'

RSpec.describe 'Hash' do
    it 'contains exactly' do
        expect({ a: 1, b: 2 }).to eq({ b: 2, a: 1 })
    end
end

内容が異なる場合も、エラーメッセージで差分を出してくれるのでわかりやすい。

     Failure/Error: expect({ a: 1, b: 2 }).to eq({ b: 2, a: 3 })

       expected: {:a=>3, :b=>2}
            got: {:a=>1, :b=>2}

       (compared using ==)

       Diff:
       @@ -1,3 +1,3 @@
       -:a => 3,
       +:a => 1,
        :b => 2,

理由

参考:https://docs.ruby-lang.org/ja/latest/class/Hash.html

リファレンスマニュアルに明記されてはいないが、ハッシュは == で比較するときに要素の順序は考慮しない

自身と other が同じ数のキーを保持し、キーが eql? メソッドで比較して全て等しく、 値が == メソッドで比較して全て等しい場合に真を返します。

{ a: 1, b: 2 } == { b: 2, a: 1 } #=> true

なので、 == で比較を行う eq を使えばいい。


ただし、ハッシュは要素の順序を保持しているので、配列に変換したりループを回したりすると違いが現れる。

ハッシュに含まれる要素の順序が保持されるようになりました。ハッシュにキーが追加された順序で列挙します。

{ a: 1, b: 2 }.to_a == { b: 2, a: 1 }.to_a #=> false
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails & Nuxt.jsのDocker環境をalpineイメージで構築

Ruby on Rails、Nuxt.js、MySQLのDocker環境を作成します。
Rails、Nuxtのalpine環境の構築手順はそれぞれだと多くあるのですが、まとまったものがあまり ない 見つからなかったので、復習を兼ねてポストを作成します。

準備

ディレクトリ作成

作業ディレクトリは任意です。

$ NEW_APP=rails-nuxt-app #任意のアプリ名
$ mkdir ${NEW_APP}
$ cd ${NEW_APP}
$ mkdir ./backend ./frontend

backend はRails用、frontend はNuxt用のディレクトリです。
まずは下記のファイルを修正していきます。

.
├ backend
│   ├ Dockerfile
│   ├ Gemfile
│   └ Gemfile.lock
│
├ frontend
│   └ Dockerfile
│
├ docker-compose.yml
└ .env

docker-compose.yml

.env

docker-compose.ymlで参照する環境変数を記載します。
ここではMySQLのrootパスワード、RailsおよびNuxt環境のホスト、ポート番号のみ定義します。
RailsとNuxtは、共にデフォルトのポートが 3000 番なので、後の利便性のためにいずれかを変えておきます。
(本記事ではNuxt側を 8080 に変更)

MYSQL_ROOT_PASSWORD=password

BACKEND_HOST=0.0.0.0
BACKEND_PORT=3000

FRONTEND_HOST=0.0.0.0
FRONTEND_PORT=8080

./docker-compose.yml

.envで定義した変数を参照しています。
docker-compose.ymlの environment、 Dockerfileの ENV で同じ環境変数が定義されていた場合は、前者が使用されます。
(本記事ではDockerfile単体でもイメージ作成できるように環境変数の記載を残していますが、docker-compose.ymlに定義されていれば問題ありません)

下記はDockerボリュームを作成します。

  • mysqlのdatadir
  • rubyのgem_home
  • nodeのnode_modules
docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7.27
    restart: always
    volumes:
      - db-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
  backend:
    build: ./backend
    ports:
      - ${BACKEND_PORT}:3000
    command: /bin/sh -c "rm -f /app/tmp/pids/server.pid && bundle exec rails s -b ${BACKEND_HOST}"
    volumes:
      - ./backend:/app
      - backend-bundle:/usr/local/bundle
    environment:
      - HOST=${BACKEND_HOST}
      - PORT=${BACKEND_PORT}
    depends_on:
      - db
    tty: true
    stdin_open: true
  frontend:
    build: ./frontend
    ports:
      - ${FRONTEND_PORT}:8080
    command: /bin/sh -c "yarn dev"
    volumes:
      - ./frontend:/app
      - frontend-node_modules:/app/node_modules
    environment:
      - HOST=${FRONTEND_HOST}
      - PORT=${FRONTEND_PORT}
    tty: true
volumes:
  db-data:
  backend-bundle:
  frontend-node_modules:

backend

Rails環境構築のためのファイルを準備します。

./backend/Gemfile

Railsのバージョンのみ指定しておきます。

Gemfile
source 'https://rubygems.org'

gem 'rails', '5.2.3'

./backend/Gemfile.lock

空ファイルをtouchしておけばOKです。

./backend/Dockerfile

Alpine Linuxのパッケージは最低限のものだけインストールします。
開発を進めるうちにgemのインストールで依存エラーが発生した場合には、不足パッケージを都度追加しましょう。

Dockerfile
FROM ruby:2.6.3-alpine

ENV RUNTIME_PACKAGES "mysql-client mysql-dev tzdata nodejs"
ENV DEV_PACKAGES "build-base curl-dev"
ENV APP_HOME /app
ENV TZ Asia/Tokyo

ENV HOST 0.0.0.0
ENV PORT 3000

WORKDIR ${APP_HOME}
ADD Gemfile ${APP_HOME}/Gemfile
ADD Gemfile.lock ${APP_HOME}/Gemfile.lock

RUN apk update \
    && apk upgrade \
    && apk add --update --no-cache ${RUNTIME_PACKAGES} \
    && apk add --update --no-cache --virtual=.build-dependencies ${DEV_PACKAGES} \
    && bundle install -j4 \
    && rm -rf /usr/local/bundle/cache/*.gem \
        && find /usr/local/bundle/gems/ -name "*.c" -delete \
        && find /usr/local/bundle/gems/ -name "*.o" -delete \
    && apk del --purge .build-dependencies \
    && rm -rf /var/cache/apk/*

COPY . ${APP_HOME}

EXPOSE ${PORT}

CMD ["rails", "server", "-b", ${HOST}]

frontend

./frontend/Dockerfile

Dockerfile
FROM node:12.9.0-alpine

ENV APP_HOME /app
ENV PATH ${APP_HOME}/node_modules/.bin:$PATH
ENV TZ Asia/Tokyo

ENV HOST 0.0.0.0
ENV PORT 8080

WORKDIR ${APP_HOME}
ADD . ${APP_HOME}

RUN apk update \
    && apk upgrade \
    && yarn install \
    && rm -rf /var/cache/apk/*

EXPOSE ${PORT}

CMD ["yarn", "dev"]

アプリケーション作成

Rails、Nuxt環境にプロジェクトを作成します。
docker-compose run を実行したタイミングで、それぞれのDockerイメージがbuildされ、さらにコンテナが立ち上がります。
--no-deps ... docker-compose.ymlで depends_on or links 指定するサービスは起動しない。
--rm ... 処理を終えたコンテナを自動的に削除。

backend

アプリケーション作成

rails new でRailsアプリケーションを作成します。
--api オプションでAPIモードにしていますが、不要な方は外してください。

$ docker-compose run --no-deps --rm backend rails new . --force --api --database=mysql --skip-bundle

DB接続のため、下記のファイルを修正します。

.
└ backend
    ├ config
    │  └ database.yml
    ├ Gemfile
    └ .env

./backend/.env

docker-compose.ymlで参照している MYSQL_ROOT_PASSWORD と同じもの設定します。

MYSQL_ROOT_PASSWORD=password

./backend/Gemfile

.envから環境変数を読み込むdotenv-railsというgemを追加します。

Gemfile
~~省略~~

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]

  gem "dotenv-rails" #追加
end

~~省略~~

./backend/config/database.yml

DBへのアクセスに使用するパスワードを、環境変数から取得します。

database.yml
~~省略~~

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: <%= ENV.fetch("MYSQL_ROOT_PASSWORD") { '' } %> #環境変数を参照するように修正
  host: db #localhostからdocker-compose.ymlのサービス名に修正

~~省略~~

frontend

アプリケーション作成

npx でNuxtアプリケーションを作成します。

後から追加/変更できるので、このタイミングではEnterキー連打でOKです。

$ docker-compose run --rm frontend npx create-nuxt-app .

~~省略~~

create-nuxt-app v2.10.0
✨  Generating Nuxt.js project in .
? Project name #アプリ名 <Enter>
? Project description #任意 <Enter>
? Author name #任意 <Enter>
? Choose the package manager #Yarn <Enter>
? Choose UI framework #None <Enter>
? Choose custom server framework #None <Enter>
? Choose Nuxt.js modules #(Nothing) <Enter>
? Choose linting tools #(Nothing) <Enter>
? Choose test framework #None <Enter>
? Choose rendering mode #Universal (SSR) <Enter>
? Choose development tools #(Nothing) <Enter>

Dockerイメージ作成

アプリケーション作成時にできたDockerボリュームは削除しておきます。

$ docker-compose down --volume
# もしくは docker volume rm ボリューム名

各ファイルを修正した状態で、Dockerイメージをビルドします。

$ docker-compose build

docker-compose.ymlで build を指定しているbackendとfrontendのイメージが作成されたことを確認します。

$ docker images --format "{{.Repository}}\t{{.CreatedSince}}" ${NEW_APP}*
rails-nuxt-app_frontend     About a minutes ago
rails-nuxt-app_backend      About a minutes ago

Hello World

最後にDockerコンテナを起動し、Rails、NuxtアプリケーションのHelloWorldを確認します、

docker-compose.ymlで定義したサービスを -d オプション(デタッチモード)でバックグラウンド起動します。

$ docker-compose up -d

プロセスが立ち上がっていることを確認します。

$ docker-compose ps
        Name                       Command               State           Ports         
---------------------------------------------------------------------------------------
rails-nuxt-app_backend_1    /bin/sh -c rm -f /app/tmp/ ...   Up      0.0.0.0:3000->3000/tcp
rails-nuxt-app_db_1         docker-entrypoint.sh mysqld      Up      3306/tcp, 33060/tcp   
rails-nuxt-app_frontend_1   docker-entrypoint.sh /bin/ ...   Up      0.0.0.0:8080->8080/tcp

失敗している場合は docker-compose logs などで原因を探りましょう。

backend

RailsアプリケーションのDBを作成します。

$ docker-compose exec backend rails db:create
Created database 'app_development'
Created database 'app_test'

ブラウザで http://localhost:3000/ を開きます。
rails-helloworld.png

frontend

ブラウザで http://localhost:8080/ を開きます。
nuxt-helloworld.png


お疲れさまでした。
次回は GraphQL の導入について投稿したいと思います。

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

Dockerを使ってRails6環境の構築をしてみる

Rails6のリリースがされていたので、自身のPCに環境を作ってみた。

(環境)

Mac(Mojave 10.14.6)

(参考)

https://docs.docker.com/compose/rails/#define-the-project
に沿って実施。
実施の時は5系の記載だったので、6に置き換えて行う。

1.Docker for Macのインストール

(インストール済みならスキップ)
https://docs.docker.com/compose/install/

2.Rails / PostgreSQLアプリケーションを設定

Docker Composeを使用してRails / PostgreSQLアプリケーションを設定する。

2-1.プロジェクト定義

開発していくディレクトリにアプリケーションを構築するために必要な4つのファイルを設定する。
・Dockerfile
・Gemfile
・Gemfile.lock
・entrypoint.sh
・docker-compose.yml

*今回はPC内のUserフォルダ配下に作成
*プロジェクト名をmyappとして設定

$ mkdir myapp
$ cd myapp
Dockerfile
FROM ruby:2.6

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

RUN apt-get update -qq && apt-get install -y nodejs postgresql-client yarn
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
Gemfile
source 'https://rubygems.org'
gem 'rails', '6.0.0'
Gemfile.lock(空のファイル)
entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

2-2.プロジェクトの構築

作成した5つのファイルを利用してdocker-compose runを実行し、アプリケーションを生成する。(ターミナルを使って実施)

$ docker-compose run web rails new . --force --no-deps --database=postgresql
:
:
:
最後にWebpackerのインストール成功メッセージが表示される
Webpacker successfully installed ? ?

アプリケーション生成後は、以下を実施する。

$ docker-compose build
:
:
:
最後に下記のようなSuccess情報が表示される
Successfully built aa99bbad99f9
Successfully tagged myapp_web:latest

2-3.データベースの設定と作成

データベースの情報を設定するために、config/database.ymlを変更し、コマンドでDBを作成する。

config/database.yml
# 設定箇所のみ抜粋
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password:
  pool: 5
:
:
development:
  <<: *default
  database: myapp_development
:
:
test:
  <<: *default
  database: myapp_test

データベースを作成する

$ docker-compose run web rake db:create

# 作成が成功すると、以下のコマンドが表示される。
Starting myapp_db_1 ... done
Created database 'myapp_development'
Created database 'myapp_test'

2-4.dockerを起動

dockerを起動し、ローカル環境のページにアクセスする。

docker-compose up

実行すると以下のコマンドが表示される。

myapp_db_1 is up-to-date
Starting myapp_web_1 ... done
Attaching to myapp_db_1, myapp_web_1
:
:
web_1  | => Booting Puma
web_1  | => Rails 6.0.0 application starting in development
web_1  | => Run `rails server --help` for more startup options
web_1  | Puma starting in single mode...
web_1  | * Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas
web_1  | * Min threads: 5, max threads: 5
web_1  | * Environment: development
web_1  | * Listening on tcp://0.0.0.0:3000
web_1  | Use Ctrl-C to stop
 => Booting Puma
 => Rails 6.0.0 application starting in development
 => Run `rails server --help` for more startup options
 Puma starting in single mode...
 * Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas
 * Min threads: 5, max threads: 5
 * Environment: development
 * Listening on tcp://0.0.0.0:3000

http://localhost:3000にアクセスして、ようこそページが表示されれば、構築完了。
スクリーンショット 2019-08-26 8.57.06.png

その他(アプリケーションの停止)

アプリケーションの停止はプロジェクトディレクトリでdocker-compose downを実行する。

・別のターミナルウィンドウを表示し、プロジェクトディレクトリから実施

$ docker-compose down
Stopping myapp_web_1 ... done
Stopping myapp_db_1  ... done
Removing myapp_web_run_b13a7c5899a1 ... done
Removing myapp_web_1                ... done
Removing myapp_web_run_5ad07400cf63 ... done
Removing myapp_db_1                 ... done
Removing network myapp_default

・downするともう一つのターミナル(docker-compose upした側)も停止の実行結果が表示される

web_1  | - Gracefully stopping, waiting for requests to finish
web_1  | === puma shutdown: 2019-08-25 23:58:46 +0000 ===
web_1  | - Goodbye!
web_1  | Exiting
myapp_web_1 exited with code 1
db_1   | 2019-08-25 23:58:47.254 UTC [1] LOG:  received smart shutdown request
db_1   | 2019-08-25 23:58:47.278 UTC [1] LOG:  background worker "logical replication launcher" (PID 28) exited with exit code 1
db_1   | 2019-08-25 23:58:47.279 UTC [23] LOG:  shutting down
db_1   | 2019-08-25 23:58:47.353 UTC [1] LOG:  database system is shut down
myapp_db_1 exited with code 0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.rubocop.yml を無視して rubocop のルールを1個だけ適用する

rubocop のルールを1個だけ適用するには、コマンドラインオプションの only を使う。たとえば文字列リテラルからなる配列を %w で置き換えるルールを適用するには下記のようにする。

rubocop --only Style/WordArray --autocorrect hoge.rb

これは、rubocop.yml に関係なく実行できるので、チーム内では %w に拘ってないのだが、自分だけはこのルールに従って変換したい、というときに使うことができる。

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

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #12 ActionMailer, アクティベーション編

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#11 プロフィール編集編
次回:準備中

こんなことが分かる

  • ユーザ新規登録時のメールによるアクティベーションの方法
  • メーラーの使い方
  • URLによるトークンとダイジェストの認証方法
  • 本番/テスト環境ごとによるメールの設定方法
  • 文字列よりシンボルが推奨される理由

一緒に勉強していきまっせ:bow:

今回の流れ

  1. アクティベーションのイメージを掴む
  2. コントローラ、属性、トークン/ダイジェストを用意する
  3. メールを生成し、メール内のURLにトークンを仕込む
  4. トークン/ダイジェストを照らし合わせ、アクティベートする
  5. ログイン時にもアクティベーションを確認する
  6. アクティベーションに関するテストを書く

アクティベーションのイメージ

今の仕様だとどんなメールアドレスでも登録が完了してしまう。
だから登録時にメールを送ってアクティベーションしたい...
どうすればよい??

というわけでどんな風に行えば実装できるのか考えてみよう。
これまでは登録に成功すると/signupから/users/:idにリダイレクトするだけだった。
lantern_lantern_signup_sitemap.png

この間にアクティベーション用のメールを挟みたい。
アプリはホーム画面にリダイレクトする。
lantern_lantern_signup_sitemap_2.png

先述しちゃったけど、メール用に必要なパスは1つ。
その他必要なビューなどは存在しない。
よってAcountActivationsコントローラと対応するリソースを生成しよう。

bash
$ rails g controller AccountActivations
config/routes.rb
Rails.application.routes.draw do
# 中略
  resources :account_activations, only: [:edit]
end

RESTに従うと、情報を更新するためにはpatchを使用する。
でもユーザにURLをクリックしてもらう以上それはgetだ。
よってupdateアクションではなくeditアクションを使用する。

アクティベーションの手順

  1. 新規作成時にトークンとダイジェストを生成する
  2. メールを生成する
  3. メール内のURLにトークンを忍ばせる
  4. URLをクリックしたらトークンとダイジェストと照らし合わせる
  5. 正しければアクティベーション済みにする

以上の動作に必要なものを列挙する。

  • アクティベーション用トークン/ダイジェスト
  • アクティベーション済みかどうかを確認する属性
  • アクティベーション用メール
  • アクティベーションするための動作コード

ポートフォリオ#9をご覧いただきたい。
アクティベーションに必要なトークンとダイジェストは同じような手順で生成できる。
対してメールを生成するメーラーは新たに使用する。
混乱する前に頭の中で整理しておくことをおすすめする。

それでは1つずつ用意していこう。

AccountActivationの属性を用意する

属性として必要なのはこの2つ。

  • activation_digest → アクティベーション用ダイジェスト
  • activated → アクティベーション済みかどうかの真偽値
bash
$ rails g migration add_activation_to_users activation_digest:string activated:boolean

真偽値はデフォルトでfalseにしておく。

db/migrate/[timestamp]_add_activation_to_users.rb
class AddActivationToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :activation_digest, :string
    add_column :users, :activated, :boolean, default: false
  end
end

マイグレーションもお忘れなく。

bash
$ rails db:migrate

トークンとダイジェストを生成する

トークンとダイジェストの生成に関しては、以前作成したUser.new_tokenとUser.digestでまかなえる。
アクティベーション用にcreate_activation_digestメソッドを作り、これらをまとめておこう。

加えてactivation_tokenは仮属性なのでattr_accessorに追加する。

/app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  # 中略
  private

    def create_activation_digest
      self.activation_token = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end
# 中略

どうやって新規作成時に生成させるか

以前emailを入力させる際before_saveを使用し、save前にアドレスをdowncaseメソッドで小文字化した。

今回はsave前ではなくcreate前に呼び出したい。
お察しのとおりRailsにはbefore_createというメソッドが用意されている。
...使おう:relaxed:

ついでにリファクタリングとして小文字化の処理もメソッド化しておく。
最終的にはこうなる。

/app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save :downcase_email
  before_create :create_activation_digest
  # 中略
  private

    def downcase_email
      email.downcase!
    end

    def create_activation_digest
      self.activation_token = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

アクティベーション用メールを生成する

メールを生成するにはメーラーというものを使う。
早速生成してみよう。
ついでに次の章で紹介するパスワードリセット用のpassword_resetメソッドも生成しておく。

bash
$ rails g mailer UserMailer account_activation password_reset

生成されたメーラーはコントローラのアクションと似ており、メーラーの各メソッドが同名のビューと対応する形。
Tutorialを参考に改変。

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

  def account_activation(user)
    @user = user
    mail to: user.email, subject: "【重要】Lantern Lanternよりアカウント有効化のためのメールを届けました"
  end

  # この部分は次章
  def password_reset
    @greeting = "Hi"
    mail to: "to@example.org"
  end
end

「なんでaccount_activationに引数与えられるの?」
とちょっと疑問に思った。

答えはシンプルでコントローラのアクションと違い、メーラーのメソッドはコントローラ内で自発的に使っていくから(っぽい)。
まあ要は普通に定義したメソッドだから使えるよねって話。

メール内のURLにトークンを仕込む

ここからはビューでメール本文を作成しよう。
必要なのはトークンとキーにするメールアドレスを仕込んだURL。
以上を踏まえるとこんな感じ。

app/views/user_mailer/account_activation.text.erb
<%= @user.name %>さんへ

Lantern Lanternにいらして下さりありがとうございます。下記のリンクをクリックして認証を済ませてください。
<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
app/views/user_mailer/account_activation.html.erb
<h1>Lantern Lantern</h1>
<p><%= @user.name %>さんへ</p>
<p>Lantern Lanternにいらして下さりありがとうございます。下記の『認証する』をクリックして認証を済ませてください。</p>
<%= link_to "認証する", edit_account_activation_url(@user.activation_token, email: @user.email) %>

さてこの部分。

edit_account_activation_url(@user.activation_token, email: @user.email)

ルートを確認するとedit_account_activationのURLパターンを教えてくれる。

bash
$ rails routes | grep edit_account_activation 
edit_account_activation GET /account_activations/:id/edit(.:format)

:id部分には引数が入る。でも今回は第2引数まである。
実はこの第2引数にハッシュを使用するとクエリパラメータを付与することができる。
結果的にこんな感じのURLを生み出す。

http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com

これでURLにトークンとメールアドレスを仕込むことができた。

メールのプレビュー

メールができたとはいえプレビューを確認したい。
そのためには設定を変える必要がある。

まずdevelopment.rbのこの箇所を、

config/environments/development.rb
  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  config.action_mailer.perform_caching = false

こうする。

config/environments/development.rb
  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = '『〜自分の環境に合わせる〜』'
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  config.action_mailer.perform_caching = false

ここに注目。

host = '〇〇'

この箇所には自分の環境によって変更する必要がある。
具体的にはdevelopment環境のURLを挿入する。
本ポートフォリオではこの部分。
lantern_lantern_development_url.png

参考にさせていただきました↓
rails チュートリアルの11章の2・2にてクラウドIDEのホスト名がわからない
Railsのconfig/enviroments配下を読んでみる

あとはプレビュー用のメソッドもいじる必要がある。

spec/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview

# https://〇〇/rails/mailers/user_mailer/account_activation
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

  def password_reset
    UserMailer.password_reset
  end
end

これで下記のリンクでプレビューを確認できるようになった。

https://〇〇/rails/mailers/user_mailer/account_activation

lantern_lantern_mail_preview.png

新規作成時にメールを送る

それではメールを送ろう。
手順としては、処理を書いた後、本番環境を整える。
しかし本番環境については、完成後に整える方がスムーズなので後回しにする。

メールを送る処理を書く

ユーザ新規作成時にメールを送るようにしよう。
メーラーで作成したメールを送るにはdeliver_nowメソッドを使用する。

UserMailer.account_activation(@user).deliver_now

ここは慣習的にメソッド化しておこう。

app/models/user.rb
class User < ApplicationRecord
# 中略
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

あとはcreateアクションに書き込む。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  # 中略
  def create
    @user = User.new(user_params)
    if @user.save
      UserMailer.account_activation(@user).deliver_now
      flash[:info] = "認証用メールを送信しました。登録時のメールアドレスから認証を済ませてください"
      redirect_to root_url
    else
      render 'new'
    end
  end

その他、flashメッセージやリダイレクト先などの変更がある。
これによりいくつかのテストが通らなくなる。
記事後半にテスト関係をまとめたので、この問題はあとで修正する。

知識を助けていただきました↓
【Rails入門】Action Mailerのメール送信を初心者向けに基礎から解説

アクティベーションを完了させる

メール内のURLをクリックしたらアクティベーション完了にしたい。
やることは2つ。

  1. トークンとダイジェストと照らし合わせるauthenticated?を編集する
  2. editアクションに処理を書く

authenticated?を編集する

以前remember_me機能を追加する際、authenticated?メソッドでトークンとダイジェストを照らし合わせていた。
アクティベーションでも同じメソッドが使えるよう、カスタマイズする。

app/models/user.rb
class User < ApplicationRecord
  # 中略
  # 以前のauthenticated?を書き換え
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end

第1引数 → rememberに使うかactivationに使うかなど
第2引数 → 指定したトークン
これで汎用性の高いメソッドに生まれ変わった。

sendを使うことで文字列をメソッドとして認識してくれる。
そこに#{}を使うことで、引数によってメソッド名を変化させるという仕組み。

再び以前書いたテストが失敗する。
これも記事後半に。

それよりもauthenticated?を使用していたcurrent_userを編集しよう。

app/helpers/sessions_helper.rb
module SessionsHelper
  # 中略
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(:remember, cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

authenticated?では第1引数を指定する際にシンボルを使用している。
ん?なんで?

文字列よりシンボルが推奨される理由

第1引数に指定する際はシンボルを推奨する。
理由はざっくりいうとこんな感じ。

  • Rubyの内部実装は、速度面で名前を整数で管理している
  • シンボルはソース上文字列、内部上整数という性質を持つ
  • よって文字列ではなくシンボルを使用する

より詳しい解説↓
Rubyの文字列とシンボルの違いをキッチリ説明できる人になりたい

editアクションに処理を書く

ようやくAccountActivationsコントローラに処理を書ける。
以下の記述でactivatedをtrueにするが、

user.update_attribute(:activated, true)

ここも慣習にならってメソッド化する。

app/models/user.rb
class User < ApplicationRecord
  # 中略
  def activate
    update_attribute(:activated, true)
  end

終わったらeditアクションに処理を書こう。

app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController

  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.activate
      log_in user
      flash[:success] = "Lantern Lanternへようこそ!"
      redirect_to user
    else
      flash[:danger] = "アクティベーションに失敗しました"
      redirect_to root_url
    end
  end
end

ここに注目。

!user.activated?

わざわざこうしているのは、アクティベーション済みにも関わらずtrueが可能になると、リンクを盗み出すだけで攻撃でアクティベーションが成功してしまうから。

ログイン時にアクティベーションを確認する

新規登録時のアクティベーションは完了した。
でもまだログイン時にアクティベーション済みかを確認する処理が書かれていない。
Sessionsコントローラを編集しよう。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  # 中略
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      if user.activated?
        log_in user
        params[:session][:remember_me] == '1' ? remember(user) : forget(user)
        redirect_back_or user
      else
        flash[:danger] = "メールを確認してアクティベーションを済ませてください"
        redirect_to root_url
      end
    else
      flash.now[:danger] = 'メールアドレスかパスワードが正しくありません'
      render 'new'
    end
  end

これでアクティベーション関係は終了となる。

本番環境でメールが届くようにする

実際のアプリでメールを送信するにはいくつかやることがある。

  • Herokuのアドオンを追加する
  • production.rbを編集する

Herokuのアドオンを追加する

Herokuからメールを送信できるようにしよう。
そのためにはアドオンを追加する。

bash
heroku addons:create sendgrid:starter

production.rbを編集する

production.rbにこういう記述があると思う。

config.action_mailer.raise_delivery_errors = false

このあたりをこんな感じに編集する。

config/environments/production.rb
# 中略
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  host = '〇〇.herokuapp.com'
  config.action_mailer.default_url_options = { host: host }
  ActionMailer::Base.smtp_settings = {
    :address        => 'smtp.sendgrid.net',
    :port           => '587',
    :authentication => :plain,
    :user_name      => ENV['SENDGRID_USERNAME'],
    :password       => ENV['SENDGRID_PASSWORD'],
    :domain         => 'heroku.com',
    :enable_starttls_auto => true
  }

〇〇の部分はHerokuで自分のアプリを開いたときのURLを挿入しよう。
(https除く)

host = '〇〇.herokuapp.com'

これで無事動作する。

メーラーのテストを書く

最後にメーラーのテストだ。
ただメーラーをテストするまでにいくつかやることがある。

  1. FactoryBotにactivated属性を与える
  2. ホスト側にドメイン名を与える

これが終わったら実際にテストといこう。

FactoryBotにactivated属性を与える

activated属性が新たに追加されたので、FactotyBotに追記しよう。
acitvatedがfalseのユーザも加えてみる。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Michael Example" }
    email { "michael@example.com" }
    password { "password" }
    password_confirmation { "password" }
    activated { true }
  end

  factory :other_user, class: User do
    name { "Sterling Archer" }
    email { "duchess@example.gov" }
    password { "foobar" }
    password_confirmation { "foobar" }
    activated { true }
  end

  factory :no_activation_user, class: User do
    name { "No Activation" }
    email { "no@activation.co.jp" }
    password { "foobar" }
    password_confirmation { "foobar" }
    activated { false }
  end
end

ホスト側にドメイン名を与える

今のままではメールの送信元のアドレスがテスト環境に存在しないことになっている。
設定を変更しよう。

config/environments/test.rb
# 中略
  config.action_mailer.delivery_method = :test
  config.action_mailer.default_url_options = { host: 'example.com' }
# 中略

メーラーテストを書く

意外と面倒くさいメーラーテスト。
自動生成されたpassword_resetに関してはコメントアウト。

spec/mailers/user_mailer_spec.rb
require "rails_helper"

RSpec.describe UserMailer, type: :mailer do

  let(:user) { create(:user) }

  describe "account_activation" do
    it "renders mails" do
      user.activation_token = User.new_token
      mail = UserMailer.account_activation(user)
      expect(mail.subject).to eq("【重要】Lantern Lanternよりアカウント有効化のためのメールを届けました")
      expect(mail.to).to eq(["michael@example.com"])
      expect(mail.from).to eq(["noreply@example.com"])
      expect(mail.body.encoded.split(/\r\n/).map{|i| Base64.decode64(i)}.join).to include("Michael Example")
    end
  end

  # describe "password_reset" do
  #   let(:mail) { UserMailer.password_reset }
  # end
end

これが気になる。

expect(mail.body.encoded.split(/\r\n/).map{|i| Base64.decode64(i)}.join).to include("Michael Example")

もちろんこうしたいのは山々だけれど、

expect(mail.body.encoded).to include("Michael Example")

これだと失敗する。理由はエンコードの関係。
このままだと訳がわからない文字列を参照することになる。
それをデコードする必要があるからこうなった訳だ。

参考にさせていただきました↓
Rails つまづき駆動投稿(TDP)
ActionMailerのメール送信テストをRSpecで行う

その他のテストを書く

テスト追加と修正を行う。

  • アクティベートしていないユーザをログイン失敗にするテストを追加と修正
  • 新規作成時のリダイレクト先をルートに修正
  • authenticated?のモデルテストを修正

アクティベートしていないユーザをログイン失敗にするテスト

ユーザをログインさせる際、アクティベート済みとそうでないユーザを分けたい。
以前テストでユーザ情報をpostする際にはメソッドを使用した。
それの修正とリファクタリングを行い、テストを完成させる。

spec/requests/users_logins_spec.rb
require 'rails_helper'

RSpec.describe "UsersLogins", type: :request do
  include SessionsHelper

  let(:user) { create(:user) }
  let(:no_activation_user) { create(:no_activation_user) }

  def post_invalid_information
    post login_path, params: {
      session: {
        email: "",
        password: ""
      }
    }
  end

  def post_valid_information(login_user, remember_me = 0)
    post login_path, params: {
      session: {
        email: login_user.email,
        password: login_user.password,
        remember_me: remember_me
      }
    }
  end

  describe "GET /login" do
    it "fails having a danger flash message" do
      get login_path
      post_invalid_information
      expect(flash[:danger]).to be_truthy
      expect(is_logged_in?).to be_falsey
      expect(request.fullpath).to eq '/login'
    end

    it "fails because they have not activated account" do
      get login_path
      post_valid_information(no_activation_user)
      expect(flash[:danger]).to be_truthy
      expect(is_logged_in?).to be_falsey
      follow_redirect!
      expect(request.fullpath).to eq '/'
    end

    it "succeeds having no danger flash message" do
      get login_path
      post_valid_information(user)
      expect(flash[:danger]).to be_falsey
      expect(is_logged_in?).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/users/1'
    end
# 中略

post_valid_informationに引数を定義したので、使用する際は引数を与えよう。
(中略している部分にも忘れずに:thumbsup:

新規作成時のリダイレクト先をルートに修正する

あとは修正だけ。

spec/requests/users_signups_spec.rb
# 中略
it "is valid signup information" do
  get signup_path
  expect { post_valid_information }.to change(User, :count).by(1)
  expect(is_logged_in?).to be_falsey
  follow_redirect!
  expect(request.fullpath).to eq '/'
  expect(flash[:info]).to be_truthy
end
spec/systems/signup_spec.rb
# 中略
it "is valid because it fulfils form information" do
  visit signup_path
  submit_with_valid_information
  expect(current_path).to eq root_path
  expect(page).to have_selector '.alert-info'
end

authenticated?のテストを修正

spec/models/user_spec.rb
# 中略
  describe "User model methods" do
    describe "authenticated?" do
      it "return false for a user with nil digest" do
        expect(user.authenticated?(:remember, '')).to be_falsey
      end
    end
  end

最後にテストを走らせておこう。

$ rails spec

これがグリーンならアクティベーションは完了。
お疲れ様ー。:relaxed:

前回:#11 プロフィール編集編
次回:準備中

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

Rails6でknockを導入してload関連のエラーが発生したときの対処法

Ruby on Rails 6.0.0 で作成したアプリケーションに、JWTトークンの発行・認証を行う目的で knock をインストールしようとしたところ、loadに関するエラーが発生したので対処法をまとめます。

発生したエラー

現象1

rails generate knock:install を実行するとwarningとerrorが出力される。
config/initializers/knock.rb の生成自体はできている。

$ rails generate knock:install                                                                                                                                                               
Running via Spring preloader in process 14337
[WARNING] Could not load generator "generators/knock/install_generator". Error: expected file /home/user/.rbenv/versions

現象2

ApplicationController にて Knock::Authenticable をincludeさせると、railsが起動できない。

$ rails c                                                                                                                                                                                     
/home/user/Documents/develop/knock_test/app/controllers/application_controller.rb:2:in `<class:ApplicationController>': uninitialized constant Knock::Authenticable (NameError)

原因

Rails6でのautoloadがzeitwerkモードに変更となったことで、autoloadの挙動がRails5以前と変わったことに起因しています。

Rails 6 zeitwerk autoload problem with gem

対処法

対処法1. 明示的にrequireする[推奨]

上記issueにある通り、明示的にソースをrequireすることでエラーを回避できます。

config/initializers/eager_load_knock.rb
require 'knock/version'
require 'knock/authenticable'

対処法2. autoloadをclassicモードに戻す[非推奨]

Rails5以前と同じ方法でautoloadさせることでもエラーを回避できます。
基本的には新しいバージョンに追従すべきなので非推奨ですが、他のgemで同様のエラーが発生する場合などはこちらの方が手っ取り早いかもしれません。

config/application.rb
require_relative 'boot'

require "rails"
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
require "rails/test_unit/railtie"

Bundler.require(*Rails.groups)

module KnockTest
  class Application < Rails::Application
    config.load_defaults 6.0
    config.api_only = true
    config.autoloader = :classic # ★追記
  end
end

まとめ

こちらの issueで何かしら修正が入ることに期待しつつ、まずは上記のような対策でエラーを回避するしかなさそうです。

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

Rails6 のちょい足しな新機能を試す70(ActiveModel::Errors#slice! 編)

はじめに

(多分)Rails 6 に追加された新機能を試す第70段。 今回は、 ActiveModel::Errors#slice! 編です。
Rails 6 では、 ActiveModel::Errors に インスタンスメソッド slice! が追加されました。

Ruby 2.6.3, Rails 6.0.0.rc2 で確認しました。Rails 6.0.0.rc2 は gem install rails -v 6.0.0rc2 --prerelease でインストールできます。
(Rails 6.0.0 がリリースされましたが、動作確認当時の最新版は、Rails 6.0.0.rc2 でした。悪しからず :bow:)

$ rails --version
Rails 6.0.0.rc2

今回は適切な例を思いつかなかったので、 rails console を使って確認します。

プロジェクトを作る

rails new rails_sandbox
cd rails_sandbox

User モデルを作る

4つの属性 name, email, country, city を持つ User モデルを作ります。

bin/rails g model User name email country city

ヴァリデーションを追加する

単純に必須入力のヴァリデーションを4つの項目に追加します。

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

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

bin/rails db:create db:migrate

rails console で確認する

rails console で確認します。
User モデルのインスタンスを作って、 validate メソッドを呼び出します。

irb(main):001:0> user = User.new
=> #<User id: nil, name: nil, email: nil, country: nil, city: nil, created_at: nil, updated_at: nil>
irb(main):002:0> user.validate
=> false

slice! メソッドで、 countrycity のメッセージだけ残します。

irb(main):003:0> errors = user.errors.slice!(:country, :city)
=> {:name=>["can't be blank"], :email=>["can't be blank"]}

full_messages でエラーメッセージを確認すると countrycity のエラーメッセージだけになっていることがわかります。

irb(main):004:0> user.errors.full_messages
=> ["Country can't be blank", "City can't be blank"]

なお、 ActiveModel::Errors#slice! の戻り値は Hash オブジェクトのため、 戻り値に対して full_messages は使えません。

irb(main):005:0> errors.full_messages
Traceback (most recent call last):
        1: from (irb):5
        NoMethodError (undefined method `full_messages' for {:name=>["can't be blank"], :email=>["can't be blank"]}:Hash)`)

試したソース

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

参考情報

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

個人開発「positiveな改善案の口コミサイト」

はじめに

未経験からエンジニアを目指しており、勉強中に開発をした個人アプリについて作成理由や機能の解説になります。現在も開発を続けておりますので、途中経過を含めて内容については開発次第、随時更新をしていきます。

http://13.114.69.119/

※何故やりたいのか、というのは個人的に結構重要視をしているので、若干サービス開発にかける想いの部分が若干長めかと思います。

作成アプリケーションの内容

開発環境

サーバーサイド言語:Ruby2.5.1
フレームワーク:Rails:5.2.2.1
フロントエンド:HTML
CSS (scss)
JavaScript (jQuery)
CSSフレームワーク:bootstrap
データベース:MySQL
サーバー:AWS(EC2 + S3)

開発のきっかけ(興味が薄ければ飛ばしていただければと思います)

食べログやオープンワーク等に代表をされる、口コミサイトは今までサービスに対しての情報が全くなかったのを見える化したことで、すごく生活を便利にしてくれたと思います。
個人的には食べログは社会人になってからずっと有料会員で、一定以上に評価をされているかを基準にすることで失敗をしない店の選び方もできるようになりました。
すごく存在価値のあることだと思っています。ただ、段々と既存の口コミサイトを利用するにつれて、問題意識を感じるようになったため、今回開発をしてみました。
特に課題に感じたのが2点あります。

①影響力の大きさを要因とした、評価の不確実性

私は前職が人材紹介の法人営業だったのですごく感じるのですが、クチコミサイトは1つの投稿が持つ、影響力がすごく大きいです。選考途中に辞退になるケースの中で、口コミサイトの評判をみて不安になってしまった、という辞退理由は実は結構多いです。
本来はこういった評価は真摯に受け止めて、しっかりと改善をしていくことが重要だと思っています。しかし、現実にはそうではない企業もあり、自社で評価を操作をしてしまっている企業が多いことも事実です。
※正規分布という確率分布があるので、一定の投稿数があれば通常は評価の点数はなだらかな評価の割合になります。
 例えば5点満点で、十分なサンプルがありながら1と5の評価が2〜4と比べて飛び抜けて多い口コミの企業とかは要注意です。また、不自然にならないように自然を装って、釣り上げる業者も存在をしています。
こういった企業が多くなると必然的に本質的な改善を目指している企業がますます評価をされない、という状況になってしまいます。本来、口コミサイトが目指す世界観としては「ユーザー側からの評価をして、透明度を増すことで誰にとってもいいサービスを作っていくこと」だと思っていますが、完全にずれてしまっていると思います。

②いつまでも残り続ける低評価が、改善をしても企業につきまとってしまう

悪い口コミがいつまでも残って影響を与え続けてしまうのは個人的には企業がすごく可愛そうだと思っています。これもまた、前職での個人的な経験からすごく感じていることです。

こんな経験がありました。

ある上場企業様から、すごく高い評価での内定が出ていた求職者の方がいたのですが、急に辞退をしたい、との連絡をいただいてしまいました。理由は案の定、クチコミサイトを見て評価が低かったのと(3年前の投稿)、過去に不正行為があったからとおっしゃっていました。
企業側の評価も元々かなり高く、何もせずに辞退になってしまうことも企業側にすごく申し訳なかったので、求職者の方に交渉をして、カウンセラーも何とか次の日の20時開始の時間で企業との面談の時間を確保しました。
企業担当の私も正直に相談をしたところ、企業側の回答は「正直に話してもらいありがとうございました、何とかします!」とのことでした。翌日、面談後に求職者の方から連絡があったのですが、なんと辞退は取り消します、とのことでした。
面談の際に何があったのかを聞くと翌日の調整依頼だったのにも関わらず、人事担当の方が急いで社長の面談をセッティングしていただき、面談では「何故不正が当時起きてしまったのか」、「経営陣を一掃した今の経営体制の強固さ」を真摯に話してくれたそうです。それに心打たれて、是非とも入社をしたい、という考えに変わったとのことでした。こちらの企業様のこの姿勢には私自身すごく好感を持ち、それから何名も入社をしていただくことができました。

この時にすごく感じたのが、「改善をしていくことの大切さ」や「真摯に対応をしていくことが人との結びつきを強くする」ということです。
この企業様にはすごくよくしていただいていたのですが、反対に応募段階で過去のクチコミサイトの情報だけで判断をされるケースもすごく多く、個人的にはかなりもったいないなと感じました。
この例では上場企業でしたが、特に創業間もない企業は課題は山積みです。そんな中でその時の評価だけを元にして判断をされていては育つ企業も育たないと思います。

本アプリの目指す世界観

上記の個人的に感じた課題感を解決して、いいサービスを提供をする意志のある企業が報われる、そんな口コミサイトを作っていければと考えております。

①改善をしていくことがいいサービス

ユーザーが現時点での企業側の評価をされるのではなく、改善をしていくことが評価をされるサービスにします。
ITの進歩で変化の早い世界になっており、現状維持はもはや退化と言われるような中では、いかにして変わっていくことができるかが重要です。
飲食店のアンケートでは、改善案の欄がよくあると思います。ただ、積極的に書いている人をほとんど見たことがありません。
中々面と向かって渡すアンケートではやりづらさもあると思いますが、企業側はすごく意見がほしいと思います。
アプリであれば心理的なハードルが下がると思うので、改善案を提言をしていくスタイルにしていきます。
ユーザー側からしても自分が提言をした内容が、実施をされればすごく嬉しいと思いますし、そうやって作っていったストーリーは唯一無二のものになってくると思います。

②前向きな表現を使う

改善案を言うということは悪く考えれば、現時点でのそのサービスにおける弱みの指摘です。ただ、本アプリではユーザーが一方的に評価をするのではなく、応援をしていけるようにしていきたいと思っています。
そのため、攻撃的な投稿が出ないようにすべてポジティブな言い回しにしています。
サービスを利用していてよかった点をGoodPointにしているのはそれほど違和感はない、と思いますが改善点をChance Pointにしているのはそのためです。
また、本アプリではGCP:cloud language natural APIを使うことによって、投稿をした内容がpositiveなものかnagativeなものか判定ができるようになっています。

③投稿された改善案の数によってマネタイズをする

本質的な改善を目指すためには双方向でのやり取りが必要不可欠になってきます。
ユーザーが勝手に投稿をしていくのは簡単ですが、それだと改善はまずしないと思うので、企業側のサービスの登録を必須にして、基本的に企業から金銭をもらうモデルを想定しています。
その際に、例えば投稿数3件もらうまでは企業側は無料、といった形にしていけば新規でやり取りをしやすいはずです。
無料でサービスを良くする改善案が入ってくるのなら企業側はまずやると思います。
そして、いくつか入ってきた口コミが良質であれば、続けたくなる(はず)ので、そのまま4件以上の投稿をしてもらうためには有料プラン、みたいな形式にしたいです。
個人的に感じることなのですが、改善をするのはすごく気持ちいいことだと思っています。「あのプロジェクト実は俺がやった」とか「あれは私が変えたんだ」とかのワードの言葉は誰しも聞いたことがあるかと思います。
いつまでも残っている、というのは特に企業側の導入を決めた人にとってはすごく誇らしいものでもあるので、離脱率はそこまで高くないのではないかと勝手に考えています。

実装内容

①実装済機能

・一覧表示機能
・詳細表示機能
・投稿機能
・削除機能
・編集機能
・管理ユーザ登録機能(deviseで2つのモデルを使用)
・画像ファイルアップロード機能
・DBテーブルリレーション管理
・ページネーション機能
・コメント機能
・ユーザー登録、削除機能ポジティブ・ネガティブ判定(GCP:cloud language natural API)
・検索機能(ransack)

②未実装機能(実装予定)

課金機能(payjp)
フォロー機能
単体テスト
統合テスト

まとめ

完成途中ではあるものの、スクールの課題と違ってゴールが決まっていないものを作るのはすごく楽しいなと感じました。まだまだ技術力的にできることに限りがあるので、もやもやを感じることは多いですが、色々思いついたことをすぐに実現をできるのはすごく魅力的です。

おまけ(似ている部分があると思ったアプリ)

サービスの質から言えば恐れ多いにもほどがありますが、個人的に似ている部分があるなと思ったサービスです。私のサービスに対してのイメージもしやすくなるかと思うので、いくつか挙げてみます。

①Insight Tech
不満を集めて「お客さまの声」や「レビューデータ」等をAIで分析をして改善につなげるサービスです。この解析をもとに実際にコンサルとかもしているそうです。大量のデータがある分、効果的な提案ができそうです。
最も概念が似ているサービスだと思うのですが、差別化のためにはこちらのサービスが大量のデータを元に分析をしているのに対して、一人ひとりとのストーリーをとにかく着目をしていく必要があると思いました。
②Unipos
前職の時に使っていたサービスです。社内で相手に感謝を伝えるサービスです。何かいい動きをした同僚に対してポイント付きのメッセージを送ることができます。同僚対同僚でのやり取りに使うサービスなので、ユーザー対企業であれば差別化にはつながるかな、とは勝手に思っています。

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

Ruby on Rails 初級

本日のRailsおさらい

順不同
考え方のみ。
細かなコマンドは別日

1、データベース
  DBの中には、テーブルという物にデータが保存
  されている。またデータをみやすくする為
  にレコード(行)とカラム(列)などIDを作
  って紐つける。

2、DBへのアクセス
  DBへはモデルがやりとりを行う。
  コントローラーがモデルに接続し、引っ張り出
  してビューにわたす。

3、DB引き出し方
  モデルからDB情報引き出す時は、
  モデルの命名規則にそって行う。
  ●モデルクラス名
   先頭は大文字の単数形=>Tweet
   モデル作成する時は全部小文字

  ●モデルクラスのファイル名
   先頭は小文字の単数形=>tweet.rb
   作成してできるファイルはこれ!

  ●テーブル名
   先頭は小文字の複数形=>Tweets

4、マイグレーションファイル
  DBにあるテーブルの設計図
  レコード(行)、カラム(列)の表を作る所

  ●レコードは大体数字
   1、2、3など
  
  ●カラムは名称が多い
   名前、年齢、性別など
   カラムは一緒に 型 も設定するよ

  ●スキーマファイル
   マイグレーションファイルのバージョン
   確認用

5、DBのテーブル編集
  今までで、テーブル作って、テーブルに名称
  も作ったから次は編集

  ●Sequel Pro
   これ使って書き込みます。
   以上!

  ●ターミナルでもできるよ
   その事をコンソールって言うねんて

6、クラスの継承
  よーわからんからまた勉強します。
  モデルの作成で作ったやつは全部
  アプリケーションリコードを継承してるみたい

7、間違えてた所
  rails c
コンソール起動してできることは
  コード実行できる
  メゾッ ド実行できる。

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

OSSマッチングサイトの開発(未完成)

2019年8月26日時点未完成 開発中!

こんにちは、ITエンジニアの田中です。

マッチングサイトのOSSとしては、Osclassが有名ですが、、、
正直使いづらい。。。でも、それ以外は有名なものがないと思っています。

そんな訳で、Ruby on Railsで汎用マッチングサイトを作ってみたいと思います。

概要

次のようなロールを想定しています。

  • サイト管理者
  • 募集主
  • 応募者

募集主が募集アイテムを作成します。
応募者は、募集アイテムを検索し応募することができます。
サイト管理者は、募集アイテムに自由に属性を作成することができます。

募集アイテムの基本属性

  • 内容

ユーザストーリー

  1. 応募者がサイトに登録できる
  2. 応募者がサイトにログインできる
  3. 募集主がサイトに登録できる
  4. 募集主がサイトにログインできる
  5. 募集主がサイトに募集アイテムを登録できる
  6. 応募者が募集アイテムを検索できる
  7. サイト運営者がサイトにログインできる
  8. サイト運営者が募集アイテムに数値の項目を追加できる(汎用項目)
  9. サイト運営者が募集アイテムに文字列の項目を追加できる(汎用項目)
  10. サイト運営者が募集アイテムに列挙型の項目を追加できる(汎用項目)
  11. 応募者が汎用項目で検索できる。
  12. 応募者が応募できる
  13. 募集主が成約できる
  14. 応募者と募集主が連絡できる

開発環境

下記のバージョンで環境を構築。

ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-linux]
rails 5.2.3

基本機能の実装

下記のチュートリアルにて、ログイン関連の基本機能を実装
https://railstutorial.jp/

具体的には、、、

  • ユーザCRUD
  • ログイン・ログアウト
  • 投稿アイテムのCRUD

データベース設計

第一ステップ
汎用募集アイテムの構造

汎用マッチングサイト.png

第二ステップ
応募の構造

第三ステップ
成約

ソース

https://github.com/you1978/generic_mattaching

MITライセンスのオープンソースプロジェクトなので
プルリクエストガンガン作ってください! 

お待ちしています。

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

Ruby on Railsを使って個人開発ver1

Ruby on Rails で個人開発

【目標】
Ruby on Railsを使って簡単なWebサイトを作成する

今回は、RailsにPostgreSQLを使用するために少しつまずいた部分を書きます。

【前提】
・Vagrant上にRails、PostgreSQLをインストール済み
・rails new #{アプリケーション名}を実行し、railsサーバーを立ち上げる
・ブラウザにアクセスすると、role "vagrant" does not exist の文字

解決方法

1.sudo -u postgres createuser --createdb vagrant を実行
postgres というユーザ名でPostgreSQLにログインする。
そしてcreatedb vagrant でDBを作成する。
これはcreatedbのrole権限の登録を行っているはず。

2.rails db:create を実行
データベースを作成する。

3.ブラウザへアクセス
無事Railsサーバーが立ち上がり、エラーが出ずにアクセスすることが出来た。

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

Ruby on Railsを使って個人開発SQL編

Ruby on Rails で個人開発

【目標】
Ruby on Railsを使って簡単なWebサイトを作成する

今回は、RailsにPostgreSQLを使用するために少しつまずいた部分を書きます。

【前提】
・Vagrant上にRails、PostgreSQLをインストール済み
・rails new #{アプリケーション名}を実行し、railsサーバーを立ち上げる
・ブラウザにアクセスすると、role "vagrant" does not exist の文字

解決方法

1.sudo -u postgres createuser --createdb vagrant を実行
postgres というユーザ名でPostgreSQLにログインする。
そしてcreatedb vagrant でDBを作成する。
これはcreatedbのrole権限の登録を行っているはず。

2.rails db:create を実行
データベースを作成する。

3.ブラウザへアクセス
無事Railsサーバーが立ち上がり、エラーが出ずにアクセスすることが出来た。

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

カラムのデータ型の変更(Rails)

環境

windows(64bit)
ruby 2.6.3p62
Rails 5.2.2
Cloud9上で開発

はじめに

$ rails g model ... などのコマンドでカラムを作成して、後からカラムのデータ型を間違えてしまったことに気づくことがあると思います。
そんな場合のために、この記事ではカラムのデータ型の変更についてご紹介します。

1. 空のマイグレーションファイルの作成

以下のようにして空のマイグレーションファイルを作成します。

$ rails g migration change_data_<カラム名>_to_<テーブル名>

僕の場合はEventのテーブルdateカラムのデータ型を変更したかったので次のようにしました。

$ rails g migration change_data_date_to_events

すると、以下のようなマイグレーションファイルが作成されます。

db/migrate/〇〇_chenge_data_date_to_events.rb
class ChangeDataDateToEvents < ActiveRecord::Migration[5.2]
  def change
  end
end

2. データ型を変更する

僕の場合、dateカラムのデータ型をdatetime型からstring型へ変更したかったので、上記のマイグレーションファイルを次のように変更しました。

db/migrate/〇〇_chenge_data_date_to_events.rb
  def change
    change_column :events, :date, :string
  end

「これで変更完了!」といいたいところですが、最後に一番大事なことを忘れずに!

3. データベースに変更内容を反映

$ rails db:migrate

おしまいです。
コンソールなどで変更内容が反映されているか確認してみてください。

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