rails_autolinkとsimple_formatでコンテンツ内URLをリンクに変換してみた

自作のTodoAppの記事内にURLを貼ってそれをurlと認識して欲しい!!!

と思い調べると、

[Ruby][Rails]テキスト内のURLをaタグに書き換える - Qiita

ふむふむ

難しいな・・

ruby on rails - Railsでユーザーが投稿したテキストにURLがあればリンクとして表示した時のXSS対策について - スタック・オーバーフロー

gemが存在するみたい!!!
これやあああ!!!

simple_formatとrails_autolinkを掛け合わせて使うと便利 - Qiita


rails_autolink

rails_autolinkgithubは不親切設計。
-> https://github.com/tenderlove/rails_autolink

どうやら元々は標準機能だったらしい。けどちょっと前にgemとして切り離されてしょげてるみたい・・

とりあえず

gem 'rails_autolink'
bundle

それでえーーーっと

f:id:yukitoku_sw:20200130212154p:plain

説明ザツゥ・・

みたところhttpから始まる塊をURLとして認識して、aタグを付けてくれるっぽい!(知らんけど)

とりあえず真似してみる

[ 一覧ページ ]

    = auto_link(@task.description)


f:id:yukitoku_sw:20200130222351p:plain

できた


あんなザツな説明でトウシロウの私でもできた。

と思ったら

f:id:yukitoku_sw:20200130222650p:plain

URLと文字がくっついちゃってるっっっっぜ!

これは、半角スペースで直りました。 くっ付けるとダメみたい


では、なぜsimple_formatと組み合わせるのか


simple_format

これはgemとかでなく、railsのヘルパーメソッドらしいです!

公式ドキュメントはなんか入場規制かかってました。 f:id:yukitoku_sw:20200130214524p:plain


なので、とっってもわかりやすかったこの記事から少し拝借

Railsヘルパーメソッド「simple_format」の使い方 - Qiita

simple_formatの機能

  • 文字列を<p>で囲む

  • 改行には<br />を付与

  • 連続した改行については、</p><p>を付与


必要性を感じない・・・


大事なのはオプションの方っぽい!

f:id:yukitoku_sw:20200130220038p:plain

Railsヘルパーメソッド「simple_format」の使い方 - Qiita
(神記事をスクリーンショット)


ん?


こっちです!

f:id:yukitoku_sw:20200130223105p:plain

Railsヘルパーメソッド「simple_format」の使い方 - Qiita
(神記事をスクリーンショット)

最後にちょろっと書いてありました!これは超大事!!(多分)


最終的なコード

= auto_link(simple_format(h(@task.description), html: { target: '_blank' }))

別タブで開きたいなーと思ってhtml: { target: '_blank' }も付けました!

これは、なぜか機能していません!!!

link_to用のではダメなのかsimple_formatよ!


答えてくれええ!


Rails6 でTodoApp作るぜ part7 タイムゾーンを日本時間にする

今回は、超絶簡単。


config.time_zone = 'Asia/Tokyo' を追加するだけです!

[ config/application.rb ]

require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module TodoApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0
    config.time_zone = 'Asia/Tokyo'   # 追加

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

以上!


f:id:yukitoku_sw:20200122211513p:plain

変更後 ->

f:id:yukitoku_sw:20200122220238p:plain


表示形式を変更する

l メソッドを使います

まず、config/application.rbtime_zonei18nを設定します。

require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module TodoApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0
    config.time_zone = 'Asia/Tokyo'
    config.i18n.default_locale = :ja
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end


そしたら、ja.ymlに変更を加える

[ config/locales/ja.yml ]

ja:
  ~~~~~~~
  time:
    formats:
      default: "%Y年%m月%d日"

フォーマットは自由に設定できます。


あとは、各ビューページの表示したい場所にlをつけるだけです。

 tbody
    - @tasks.each do |task|
      tr
        td = task.done? ? 'Done' : 'Todo'
        td = link_to task.title, task
        td = l task.created_at
        td = l task.updated_at
        td


こんな感じ〜 f:id:yukitoku_sw:20200122223123p:plain


以上だ


Rails 6 でTodoApp作る part6

今回は、Taskテーブルにdone:booleanを加えて、todoリスト・doneリストに分ける

※次回予告は確定ではありません。

Taskテーブルにdoneカラムを追加

% rails g migration add_done_to_tasks done:boolean

そして、

  • デフォルトfalse

  • nullもfalse

を追加します

[ Migrationファイル ]

class AddDoneToTasks < ActiveRecord::Migration[6.0]
  def change
    add_column :tasks, :done, :boolean, default: false, null: false
  end
end
% rails db:migrate


Seedを変更

今のサンプルデータには、doneカラムの指定がないのでチョコっといじります。

[ db/seeds.rb ]

(省略)

users = User.all

# todoタスク
15.times do
title = Faker::Beer.brand
description = Faker::Movies::BackToTheFuture.quote
users.each { |user| user.tasks.create!(title: title, description: description) }
end

# doneタスク
15.times do
title = Faker::Beer.brand
description = Faker::Movies::BackToTheFuture.quote
users.each { |user| user.tasks.create!(title: title, description: description, done: true) }
end

doneカラムはdefault: trueとしました。 なので、todoタスク用のサンプルデータは特に変更しません。
doneタスク用のサンプルデータの方にだけdone: trueを指定しておきます。


Routingの編集

/tasks/indexでタスク一覧を表示しているが、

  • /tasks/todo -> todo一覧

  • /tasks/done -> done一覧

となるようにしたい!!


そこで、resourecesメソッドのcollectionを使います。

[ config/routes.rb ]

  resources :tasks do
    collection do
      get :todo, :done
    end
  end

collectionの他にmemberもありますが、今回は一覧ページを作りたいのでcollectionを使いました。

resourecesメソッドについて詳しくは↓

https://pikawaka.com/rails/resources#member%E3%81%A8collection


collectionにより、todoアクションdoneアクションが追加されます。

% rails routes | grep task
                           todo_tasks GET    /tasks/todo(.:format)                                                                    tasks#todo
                           done_tasks GET    /tasks/done(.:format)                                                                    tasks#done
                                tasks GET    /tasks(.:format)                                                                         tasks#index
                                      POST   /tasks(.:format)                                                                         tasks#create
                             new_task GET    /tasks/new(.:format)                                                                     tasks#new
                            edit_task GET    /tasks/:id/edit(.:format)                                                                tasks#edit
                                 task GET    /tasks/:id(.:format)                                                                     tasks#show
                                      PATCH  /tasks/:id(.:format)                                                                     tasks#update
                                      PUT    /tasks/:id(.:format)                                                                     tasks#update
                                      DELETE /tasks/:id(.:format)                                                                     tasks#destroy

こんな感じ〜


Controllerの編集

todoアクションとdoneアクションを実装していきます。

[ tasks_controller.rb ]

class TasksController < ApplicationController
  before_action :set_task, only: [:show, :edit, :update, :destroy]

  def index
    @q = current_user.tasks.ransack(params[:q])
    @tasks = @q.result(distinct: true).recent.page(params[:page]).per(10)
  end

  def todo
    @q = current_user.tasks.where(done: false).ransack(params[:q])
    @tasks = @q.result(distinct: true).recent.page(params[:page]).per(10)
  end

  def done
    @q = current_user.tasks.where(done: true).ransack(params[:q])
    @tasks = @q.result(distinct: true).recent.page(params[:page]).per(10)
  end

(省略)

indexの中身をtododoneに移植します。

そして

  • todoには、where(done: falseを加える

  • doneには、where(done: true)を加える

  • 移植が終わったらindexアクションを消す

これで、コントローラの準備はOk


Viewの実装

viewを修正していくう

まずは、tasks/index.html.slim

[ tasks/index.html.slim ]

ul.nav.nav-tabs.mb-3
  li.nav-item = link_to '新規登録', new_task_path, class: 'nav-link'
  li.nav-item = link_to 'Todoリスト', todo_tasks_path, class: 'nav-link'
  li.nav-item = link_to 'Doneリスト', done_tasks_path, class: 'nav-link'
.row.justify-content-end
  = search_form_for @q do |f|
    .form-inline
      = f.label :title_cont, 'タイトル', class: 'my-1 mr-2'
      = f.search_field :title_cont, class: 'my-1 mr-sm-2 form-control'
      = f.submit '検索', class: 'btn btn-primary my-1'
.mb-3
  = paginate @tasks
  = page_entries_info @tasks
table.table
  thead.thead-default
    tr
      th 状況
      th タイトル
      th 登録日
      th 編集日
      th
  tbody
    - @tasks.each do |task|
      tr
        td = task.done? ? 'Done' : 'Todo'
        td = link_to task.title, task
        td = task.created_at
        td = task.updated_at
        td
          = link_to '編集', edit_task_path(task), class: 'btn btn-primary mr-3'
          = link_to '削除', task, method: :delete, data: { confirm: "タスク「#{task.title}」を削除します。よろしいですか?" }, class: 'btn btn-danger'


修正内容

  • h1 タスク一覧を消す

  • navリンク

  • テーブル内th 状況 を追加

  • テーブル内td = task.done? ? 'Done' : 'Todo'を追加

4カ所変更を加えました。


そしたら、2つのファイルを作成

  • app/tasks/todo.html.slim

  • app/tasks/done.html.slim


[ app/tasks/todo.html.slim ]

h1 Todo一覧

= render 'index'

[ app/tasks/done.html.slim ]

h1 Done一覧

= render 'index'


はい。

tasks/index.html.slimをパーシャルにしてやろうか(デーモン)

tasks/index.html.slimをrenameしてtasks/_index.html.slimに変更します


todo一覧 f:id:yukitoku_sw:20200122211513p:plain

done一覧 f:id:yukitoku_sw:20200122211559p:plain

navのactiveのやり方がわからない・・・


次は、tasks/_form.html.slimを変更する

= form_with model: task, local: true do |f|
  .form-check.mb-2
    = f.label :done, class: 'form-check-label' do
      = f.check_box :done, class: 'form-check-input'
      | done
  .form-group
    = f.label :title, 'タイトル'
    = f.text_field :title, class: 'form-control', id: 'task_title'
  .form-group
    = f.label :description, '詳細'
    = f.text_area :description, class: 'form-control', id: 'task_description'
  .form-group
    = f.label :image, '画像'
    = f.file_field :image, class: 'form-control', id: 'task_image'
  = f.submit '登録', class: 'btn btn-success'

form内に.form-checkの部分を追加

ちょっとわかりにくいがまーよし

f:id:yukitoku_sw:20200122212549p:plain

新規登録時にdoneを付ければ、そのままdone一覧に入るし、doneを付けなければtodo一覧に入る。もちろんeditからdoenにチェックを入れればdone一覧に移動する


各ページに貼ったtasks_pathへのリンクを編集する

[ show.html.slim & edit.html.slim ]

.nav.justify-content-end
  - if @task.done?
    = link_to 'Done一覧', done_tasks_path, class: 'nav-link'
  - else
    = link_to 'Todo一覧', todo_tasks_path, class: 'nav-link'

これで、選択した@taskによってリンク先を変更することができる


ヘッダーとnew.html.slimにあるtasks_pathへのリンクには@taskが存在しないので、todo_tasks_pathを指定する


今回は以上です。

ありがとうございました!


次回、タイムゾーンを日本時間にする

Rails 6 でTodoApp作る part5

プロフィールの編集を制限する

自分以外のユーザーの編集をできなくする

現状、ログインしていれば全てのユーザーの編集ページに入ることができます。


ユーザー"panda"でログイン中

[ 自分のページ ] f:id:yukitoku_sw:20200120201930p:plain

[ rakudaさんのページ ] f:id:yukitoku_sw:20200120202003p:plain

これを対処します

  • 自分のページのみ編集ページへのリンクを表示する

  • URL直打ちでのアクセスを防ぐ


自分のページのみ編集ページへのリンクを表示する

[ views/users/show.html.slim ]

h1 アカウントの詳細

.nav.justify-content-end
  = link_to '一覧', users_path, class: 'nav-link'

table.table.table-hover
  tbody
    tr
      th 名前
      td = @user.name
    tr
      th メールアドレス
      td = @user.email
    tr
      th 登録日
      td = @user.created_at
    tr
      th 更新日
      td = @user.updated_at

- if current_user.id == @user.id     # 追加
  = link_to '編集', edit_user_path, class: 'btn btn-primary mr-3'

- if current_user.id == @user.idによりログイン中のユーザーのidと詳細ページのユーザーのidが同じ時のみ編集ページへのリンクが表示されるようになります


ユーザー"panda"でログイン中

[ 自分のページ ] f:id:yukitoku_sw:20200120203359p:plain

[ rakudaさんのページ ] f:id:yukitoku_sw:20200120203420p:plain

自分のshowページでのみ、editページへのリンクが表示されるようになりました!


URL直打ちでのアクセスを防ぐ

[ users_controller.rb ]

class UsersController < ApplicationController
  before_action :correct_user, only: [:edit, :update, :destroy]

  def index
    @users = User.all
  end

  (省略)

  private

    def user_params
      params.require(:user).permit(:name, :email, :password, :password_confirmation)
    end

    # before_action
    def correct_user
      user = User.find(params[:id])
      redirect_to root_url if current_user != user
    end
end

correct_userをprivateメソッドとして定義し、before_actionで呼び出します。 users_controller内で[:edit, :update, :destroy]のアクションを実行する前に、実行しようとするユーザー(current_user)と編集されようとしているユーザーが一致していなければroot_urlに飛ばします。

これで自分のユーザーアカウントを他のユーザーに編集されないようになります。


フィルタを使い重複を避ける

taskコントローラーを作る際に作ったフィルタをUserバージョンでもう一度やります。

[ users_controller.rb(現在) ]

class UsersController < ApplicationController
  before_action :correct_user, only: [:edit, :update, :destroy]

  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to users_url, notice: "ユーザー「#{@user.name}」を登録しました。"
    else
      render :new
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      redirect_to users_url, notice: "ユーザー「#{@user.nsame}」を更新しました。"
    else
      render :edit
    end
  end

  def destroy
    @user = User.find(prams[:id])
    @user.destroy
    redirect_to root, notice: "ユーザー「#{@user.name}」を削除しました。"
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password, :password_confirmation)
    end

    # before_action
    def correct_user
      user = User.find(params[:id])
      redirect_to root_url if current_user != user
    end
end

上記は現在のusers_controllerです。 @user = User.find(prams[:id])が連発されているのでフィルタを使いすっきりさせます。


before_action :set_user, only: [:show, :edit, :update, :destroy]

def set_user
  @user = User.find(prams[:id])
end

set_userを定義し、before_actionで呼び出します。
指定されたアクションが実行される前に@user = User.find(prams[:id])を行います。 そのため、各アクションから@user = User.find(prams[:id])の記述を消すことができるので、各アクションの中身がシンプルになります。
トータルの行数は増えてしまいますが、、、

[ users_controller.rb(変更後) ]

class UsersController < ApplicationController
  before_action :correct_user, only: [:edit, :update, :destroy]
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  def index
    @users = User.all
  end

  def show
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to users_url, notice: "ユーザー「#{@user.name}」を登録しました。"
    else
      render :new
    end
  end

  def edit
  end

  def update
    if @user.update(user_params)
      redirect_to users_url, notice: "ユーザー「#{@user.nsame}」を更新しました。"
    else
      render :edit
    end
  end

  def destroy
    @user.destroy
    redirect_to root, notice: "ユーザー「#{@user.name}」を削除しました。"
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password, :password_confirmation)
    end

    # before_action
    def correct_user
      user = User.find(params[:id])
      redirect_to root_url if current_user != user
    end

    def set_user
      @user = User.find(prams[:id])
    end
end


i18nで日本語対応にする

所々、英語が表示されちゃってる部分を日本語にしていくう


まず、

[ Gemfile ]

gem 'rails-i18n'
bundle

以前設定したlocale.rbをいちよう確認。

[ config/initializers/locale.rb ]

Rails.application.config.i18n.default_locale = :ja


これだけで日本語になります。(完全な英語の状態を撮り忘れました。)

f:id:yukitoku_sw:20200120220505p:plain


しかし、まだちょっと残っている英語も日本語にします。

config/locales/ja.ymlと言うファイルを作成します。

そして、内容修正

[ config/locales/ja.yml ]

ja:
  activerecord:
    models:
      task: タスク
    attributes:
      task:
        title: タイトル
        description: 詳しい説明
  • モデル名はactiverecoredのmodelsの中に
  • 属性名はactiverecoredのattributesの中に定義します。

サーバーを再起動すると・・

f:id:yukitoku_sw:20200120221050p:plain

ちょっとあれですね・・・

f:id:yukitoku_sw:20200120232443p:plain

こんな感じにします。

[ tasks_controller.rb ]

- if task.errors.any?
  div#error_explanation
    .alert.alert-danger
      = "#{task.errors.count}種類のエラーがあります"
      ul
        - task.errors.full_messages.each do |message|
          li = message
      
= form_with model: task, local: true do |f|
  (省略)

application.html.slimも変更しなくては・・

[ layouts/application.html.slim ]

doctype html
html
  head
    title
      | TodoApp
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
  body
    = render 'layouts/header'
    .container
      - flash.each do |message_type, message|
        = content_tag(:div, message, class: "alert alert-#{message_type}")
      = yield

noticeからflashに変更したのでコントローラーも変更しなくては・・・

[ tasks_controller.rb ]

(省略)
  def create
    @task = current_user.tasks.new(task_params)
    if @task.save
      flash[:success] = "タスク「#{@task.title}」を登録しました。"
      redirect_to tasks_url
    else
      render :new
    end
  end

(省略)

  def update
    if @task.update(task_params)
      flash[:success] = "タスク「#{@task.title}」を更新しました!"
      redirect_to tasks_url
    else
      render :edit
    end
  end

  def destroy
    @task.destroy
    flash[:warning] = "タスク「#{@task.title}」を削除しました。"
    redirect_to tasks_url
  end

(省略)


こっちも・・・

[ users_controller.rb ]

(省略)
  
  def create
    @task = current_user.tasks.new(task_params)
    if @task.save
      flash[:success] = "タスク「#{@task.title}」を登録しました。"
      redirect_to tasks_url
    else
      render :new
    end
  end

(省略)

  def update
    if @task.update(task_params)
      flash[:success] = "タスク「#{@task.title}」を更新しました!"
      redirect_to tasks_url
    else
      render :edit
    end
  end

  def destroy
    @task.destroy
    flash[:warning] = "タスク「#{@task.title}」を削除しました。"
    redirect_to tasks_url
  end

(省略)


あと、sessions_controllerにも・・
flash.now[:danger] = 'メールアドレスとパスワードの組み合わせが存在しません'も追加してます!

[ sessions_controller.rb ]

(省略)

  def create
    user = User.find_by(email: session_params[:email])

    if user&.authenticate(session_params[:password])
      session[:user_id] = user.id
      redirect_to root_url, notice: 'ログインしました'
    else
      flash.now[:danger] = 'メールアドレスとパスワードの組み合わせが存在しません'
      render :new
    end
  end

  def destroy
    reset_session
    flash[:success] = 'ログアウトしました'
    redirect_to login_url
  end

(省略)


usersformも直しておきます。

[ users/_form.html.slim ]

- if user.errors.any?
  div#error_explanation
    .alert.alert-danger
      = "#{task.errors.count}種類のエラーがあります"
      ul
        - user.errors.full_messages.each do |message|
          li = message
      
= form_with model: user, local: true do |f|
(省略)


今日はここまで、、


次回、rspec導入

Rails 6 でTodoApp作る part4

機能武装していきます。

まず初めに

Seedsでサンプルデータ投入

fakerを利用してサンプルデータを大量に投入する。

Gemfileにgem 'faker'を記入してbundle

[ Gemfile ]

gem 'faker'
% bundle

seedsファイルを編集します。

[ db/seeds.rb ]

# ユーザー
User.create!(
  [
    {
      name: 'panda',
      email: 'panda@example',
      password: 'password',
      password_confirmation: 'password',
    },
    {
      name: 'rakuda',
      email: 'rakuda@example.com',
      password: 'password',
      password_confirmation: 'password'
    },
    {
      name: 'tamanegi',
      email: 'tamanegi@example.com',
      password: 'password',
      password_confirmation: 'password'
    },
  ]
)

  # タスク
  users = User.all
  30.times do
  title = Faker::JapaneseMedia::SwordArtOnline.game_name
  description = Faker::JapaneseMedia::SwordArtOnline.location
  users.each { |user| user.tasks.create!(title: title, description: description) }
end

ユーザーを3人作成して、各ユーザーに30タスクを作ります。

% rails db:reset db:seed
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'

fakerについては ↓
GitHub - faker-ruby/faker: A library for generating fake data such as names, addresses, and phone numbers.

いろいろなサンプルデータがあって面白い


Ransackで検索機能

Tasksの一覧画面に検索機能をつけていくっ

GitHub - activerecord-hackery/ransack: Object-based searching.
に書いてある通り進めていきます。

Gemfileにransackを記入しbundle

[ Gemfile ]

gem 'ransack'
bundle

ここで、サーバーを起動中であれば再起動する。


続いて、コントローラーのindexアクションを編集します

[ tasks_controller.rb ]

  def index
    @q = current_user.tasks.ransack(params[:q])
    @tasks = @q.result(distinct: true).recent
  end

ビューに検索フォームを追加します

f:id:yukitoku_sw:20200119213925p:plain

[ tasks/index.html.slim ]

h1 タスク一覧

.nav
  = link_to '新規登録', new_task_path, class: 'nav-link'

.row.justify-content-end
  = search_form_for @q, class: 'mb-5 ' do |f|
    .form-inline
      = f.label :title_cont, 'タイトル', class: 'my-1 mr-2'
      = f.search_field :title_cont, class: 'my-1 mr-sm-2'
      = f.submit '検索', class: 'btn btn-primary my-1'
table.table
  thead.thead-default
    tr
      th タイトル
  (省略)

f:id:yukitoku_sw:20200119214116p:plain

検索機能がちゃんと動いていることもKiritoで確認!

[ コンソール ]

  Parameters: {"q"=>{"title_cont"=>"kirito"}, "commit"=>"検索"}
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/application_controller.rb:8:in `current_user'
  Rendering tasks/index.html.slim within layouts/application
  Task Load (0.3ms)  SELECT DISTINCT "tasks".* FROM "tasks" WHERE "tasks"."user_id" = ? AND "tasks"."title" LIKE '%kirito%' ORDER BY "tasks"."created_at" DESC  [["user_id", 1]]

logを確認すると"title" LIKE '%kirito%と書いてあります
検索した文字列を含むものは全てokみたいなので試しにkiだけで検索してみます。

f:id:yukitoku_sw:20200119214751p:plain

もっと表示されるかと思いましたが・・・
タイトルにkiを含む全てのtaskが表示されました!


ActiveStorageで画像添付機能

ActiveStorageとは、ファイル管理gemのことです。 Rails5.2から標準搭載となったのでGemfileに書いてbundleする作業は不要です
environment環境で利用する場合は保存先の設定をする必要があります。今回はdevelopment環境で使う設定でやります。environment環境の設定は後日やります・・・

詳しくは、Active Storage の概要 - Railsガイド

gem自体はインストール不要ですが、下記コマンドでActiveStoragを使えるようにする必要があります。

 % rails active_storage:install
Copied migration 20200119130415_create_active_storage_tables.active_storage.rb from active_storage

すると、下記マイグレーションファイルが作成されます。

[ db/migration/??????????create_active_storage_tables.active_storage.rb ]_

# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
  def change
    create_table :active_storage_blobs do |t|
      t.string   :key,        null: false
      t.string   :filename,   null: false
      t.string   :content_type
      t.text     :metadata
      t.bigint   :byte_size,  null: false
      t.string   :checksum,   null: false
      t.datetime :created_at, null: false

      t.index [ :key ], unique: true
    end

    create_table :active_storage_attachments do |t|
      t.string     :name,     null: false
      t.references :record,   null: false, polymorphic: true, index: false
      t.references :blob,     null: false

      t.datetime :created_at, null: false

      t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end
  end
end

2種類のtableが作成されてそこに Active Storage の概要 - Railsガイド


準備が整いましたので実装していきます

[ models/task.rb ]

class Task < ApplicationRecord
  validates :title, presence: true, length: { maximum: 30 }
  validates :description, presence: true

  has_one_attached :image   # 追加
  belongs_to :user

  scope :recent, -> { order(created_at: :desc) }
end

has_one_attachedメソッドを使い、1つのTaskにつき1つの画像を添付できるようになります。その画像はimageとして呼ぶことができます。


画像を添付するためのformを追加します。

[ views/tasks/_form.html.slim ]

(省略)
= form_with model: task, local: true do |f|
  .form-group
    = f.label :title, 'タイトル'
    = f.text_field :title, class: 'form-control', id: 'task_title'
  .form-group
    = f.label :description, '詳細'
    = f.text_area :description, class: 'form-control', id: 'task_description'
  .form-group    # 追加
    = f.label :image, '画像'   # 追加
    = f.file_field :image, class: 'form-control', id: 'task_image'   # 追加
  = f.submit '登録', class: 'btn btn-success'

f:id:yukitoku_sw:20200119224109p:plain

こんな感じ


StrongParametersにimageを追加します

[ tasks_controller.rb ]

(省略)
 private

    def task_params
      params.require(:task).permit(:title, :description, :image)   <- :image追加
    end
(省略)

viewにimageできるようにする

[ views/tasks/show.html.slim ]

(省略)

      th 詳細
      td = @task.description
    tr
      th 画像
      td = image_tag @task.image if @task.image.attached?
    tr
      th 登録日
      td = @task.created_at
(省略)

全てのTaskに画像添付したいわけではないので、if @task.image.attached?をつけます。
これがないと画像がなかったときにエラーとなってしまいます。

機能が正常に動くか確認します。

f:id:yukitoku_sw:20200119225121p:plain

無事、画像の添付に成功しました!!


Kaminariでページネーション

現在一覧画面に30件のタスクが表示されています。
これから増えていけば増えていくほどページの読み込みが遅くなります。

こりゃいかん


[ Gemfile ]

gem 'kaminari'
% bundle

GitHub - kaminari/kaminari: ⚡ A Scope & Engine based, clean, powerful, customizable and sophisticated paginator for Ruby webapps Kaminariの詳細


さっそくコントローラーの設定から行きますっ

[ tasks_controller.rb ]

(省略)

  def index
    @q = current_user.tasks.ransack(params[:q])
    @tasks = @q.result(distinct: true).recent.page(params[:page])
  end

(省略)


続いてview

[ views/tasks/index.html.slim ]

(省略)
.row.justify-content-end
  = search_form_for @q, class: 'mb-5 ' do |f|
    .form-inline
      = f.label :title_cont, 'タイトル', class: 'my-1 mr-2'
      = f.search_field :title_cont, class: 'my-1 mr-sm-2'
      = f.submit '検索', class: 'btn btn-primary my-1'
.mb-3
  = paginate @tasks
  = page_entries_info @tasks
table.table
  thead.thead-default
    tr
      th タイトル
      th 登録日
 
(省略)

こんな感じ

f:id:yukitoku_sw:20200119231820p:plain

gemをインストールしたらサーバーを再起動するのは基本中の基本です。

[ コンソール ]

サーバー再起動

そして

f:id:yukitoku_sw:20200119232022p:plain

うまく表示されました!


しか〜し!

  • なぜ英語なのか

  • デフォルトの表示件数が25件/ページなのでもっと減らしたい


表記を日本語にする

config/localeskaminari.ja.ymlと言うファイルを作成します。

そして、ちょっと面倒ですが下記内容を記述します

[ config/locales/kaminari.ja.yml ]

ja:
  views:
    pagination:
      first: "&laquo; 最初"
      last: "最後 &raquo;"
      previous: "&lsaquo; 前"
      next: "次 &rsaquo;"
      truncate: "&hellip;"
  helpers:
    page_entries_info:
      one_page:
        display_entries:
          zero: "%{entry_name}がありません"
          one: "1件の%{entry_name}が表示されています"
          other: "%{count} 件の%{entry_name}が表示されています"
      more_pages:
          display_entries: "全%{total} 件中 %{first}&nbsp;-&nbsp;%{last} 件の%{entry_name}が表示されています"

次に、

config/initializerslocale.rbと言うファイルを作成し、下記を記述します

[ config/initializers/locale.rb ]

Rails.application.config.i18n.default_locale = :ja

そして、

[ コンソール ]

サーバー再起動

すると・・・

f:id:yukitoku_sw:20200119233440p:plain

tasks以外は日本語になりました!(tasksはまた別の場所で設定する必要があるため今回はパス)

しかしデザインが今市なので、Bootstrapを使います。(bootstrapは導入済みです)

[ コンソール ]

% rails g kaminari:views bootstrap4
Running via Spring preloader in process 2902
      downloading app/views/kaminari/_first_page.html.slim from kaminari_themes...
      create  app/views/kaminari/_first_page.html.slim
      downloading app/views/kaminari/_gap.html.slim from kaminari_themes...
      create  app/views/kaminari/_gap.html.slim
      downloading app/views/kaminari/_last_page.html.slim from kamina
(省略)

こんだけです

f:id:yukitoku_sw:20200119234041p:plain

いい感じになりました^^


残るは表示件数の調整

表示件数の変更方法は3通り?あるんですが、一番簡単なやつ

[ tasks_controller.rb ]

(省略)

  def index
    @q = current_user.tasks.ransack(params[:q])
    @tasks = @q.result(distinct: true).recent.page(params[:page]).per(6)
  end

(省略)

per(6)をつけるだけです *()内の数字を自由に入れてください
スクロールなしで表示できるのは6件でした!

f:id:yukitoku_sw:20200119235041p:plain


今回は以上です!


次回、後でやるをやるお

Rails 6 でTodoApp作るぜ part3

今回はUserを作ってログインとかするぜ

Userモデル作成

テーブル設計

名称 カラム名 データ型
名前 name string
メールアドレス email string
パスワード password_digest string


% rails g model user name:string email:string password_digest:string


今回はしっかりnullつけます


[ db/migrate/?????????.create_user.rb ]

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.string :password_digest, null: false

      t.timestamps
      t.index :email, unique: true
    end
  end
end


t.index :email, unique: trueは同じメールアドレスのユーザーが複数人保存されないためにつけています。

% rails db:migrate

パスワードをdigestで保存する

まずはbcryptというハッシュ関数を提供するgemをbundleします。
bcryptはもともとGemfileに記述されています。しかし、なぜかコメントアウトされています。なのでコメントインしてあげてbundleします

[ Gemfile ]

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.5'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.2', '>= 6.0.2.1'
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4'
# Use Puma as the app server
gem 'puma', '~> 4.1'
# Use SCSS for stylesheets
gem 'sass-rails', '>= 6'
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
gem 'webpacker', '~> 4.0'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7'            <- これ!

# Use Active Storage variant
# gem 'image_processing', '~> 1.2'

# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.2', require: false

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]
end

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'
end

group :test do
  # Adds support for Capybara system testing and selenium driver
  gem 'capybara', '>= 2.15'
  gem 'selenium-webdriver'
  # Easy installation and use of web drivers to run system tests with browsers
  gem 'webdrivers'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

gem 'slim-rails'
gem 'html2slim'
% bundle


次にuser.rbを編集します

[ app/models/user.rb ]

class User < ApplicationRecord
  has_secure_password

  validates :name, presence: true
  validates :email, presence: true, uniqueness: true
end

has_secure_passwordbcryptがインストールされていないと使用できません。
has_secure_passwordを使うと先ほど作成したuserテーブルpasswordpassword_confirmationというカラムが作られます。目には見えませんが確実に作られます。
passwordに入力された値とpassword_confirmationに入力された値が等しかった場合はその値をハッシュ化してpassword_digestに登録されるという仕組みです。

それと、validatesです。 名前やメールアドレスが空のとき、メールアドレスが既に存在するときは保存に失敗してエラーが発生するようにしています。


users_controllerの作成

% rails g controller users index show new edit

routesの設定

users_controllerにもresourcesを使い、tasks_controllerと同じく一般的なCRUD機能を持たせます


[ config/routes.rb ]

Rails.application.routes.draw do

  resources :users

  root 'tasks#index'
  get 'tasks/index', to: 'tasks#index'
  resources :tasks
end


controllerの設定

routesで設定したとうり、シンプルなCRUD機能を作ります


[ app/controllers/users_controller.rb ]

class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to users_url, notice: "ユーザー「#{@user.name}」を登録しました。"
    else
      render :new
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      redirect_to users_url, notice: "ユーザー「#{@user.nsame}」を更新しました。"
    else
      render :edit
    end
  end

  def destroy
    @user = User.find(prams[:id])
    @user.destroy
    redirect_to root, notice: "ユーザー「#{@user.name}」を削除しました。"
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password, :password_confirmation)
    end

end

viewsの作成

まず初めに、コンソールから適当にユーザーを作っておきます。

name panda
email panda@example.com
password password
password_confirmation password

めっちゃ適当!

% rails c   
Running via Spring preloader in process 60245
Loading development environment (Rails 6.0.2.1)
>> user = User.new(name: "panda", email: "panda@example.com", password: "password", password_confirmation: "password")
   (1.3ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: "panda", email: "panda@example.com", password_digest: [FILTERED], created_at: nil, updated_at: nil>
>> user.save
   (0.1ms)  begin transaction
  User Create (1.8ms)  INSERT INTO "users" ("name", "email", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "panda"], ["email", "panda@example.com"], ["password_digest", "$2a$12$UPS/sV/yXgtL1TJ8vStzJuuDRgTvqPPGbPJVkFpT2K6Pp.dnwSZiO"], ["created_at", "2020-01-18 11:37:54.068311"], ["updated_at", "2020-01-18 11:37:54.068311"]]
   (2.0ms)  commit transaction
=> true

みてもらうとわかりますが、newではpassword: "password", password_confiramtion: "password"と入力しています。しかし、saveでは、["password_digest", "$2a$12$UPS/sV/yXgtL1TJ8vStzJuuDRgTvqPPGbPJVkFpT2K6Pp.dnwSZiO"]と保存されています。これがhas_secure_passwordbcryptのパワーです。このハッシュ化によりdbを悪者に覗かれても元のパスワードを盗むことは不可能。らしいです。


それでは、viewsを順番に紹介していきます。(taskとほぼ同じです。)

[ users/index.html.slim ]

f:id:yukitoku_sw:20200118211425p:plain

h1 アカウント一覧

.nav.justify-content-end
  = link_to '新規登録', new_user_path, class: 'nav-link'

table.table
  thead.thead-default
    tr
      th 名前
      th 登録日
      th 編集日
  tbody
    - @users.each do |user|
      tr
        td = link_to user.name, user
        td = user.created_at
        td = user.updated_at

ユーザー一覧となるのが嫌いなのでアカウントにしました。この気持ちわかります??

編集と削除のリンクはつけていません。自分のアカウントを他の人に勝手に消されたら困るので


[ users/show.html.slim ]

f:id:yukitoku_sw:20200118211842p:plain

h1 アカウントの詳細

.nav.justify-content-end
  = link_to '一覧', users_path, class: 'nav-link'

table.table.table-hover
  tbody
    tr
      th 名前
      td = @user.name
    tr
      th メールアドレス
      td = @user.email
    tr
      th 登録日
      td = @user.created_at
    tr
      th 更新日
      td = @user.updated_at

= link_to '編集', edit_user_path, class: 'btn btn-primary mr-3'

詳細ページでは編集だけ残しています。後で、自分のページでしか表示されないようにします。

基本的にtaskで作ったviewページをコピペしてます。


newとeditの前に_formを作ります。

[ users/_form.html.slim ]

- if user.errors.present?
  ul#error_explanation
    - user_errors.full_messages.each do |message|
      li = message
      
= form_with model: user, local: true do |f|
  .form-group
    = f.label :name, '名前'
    = f.text_field :name, class: 'form-control'
  .form-group
    = f.label :email, 'メールアドレス'
    = f.text_field :email, class: 'form-control'
  .form-group
    = f.label :password, 'パスワード'
    = f.password_field :password, class: 'form-control'
  .form-group
    = f.label :password_confirmation, 'パスワード(確認)'
    = f.password_field :password_confirmation, class: 'form-control'
  = f.submit '登録', class: 'btn btn-success'

formはtaskと中身が違うのでお気をつけて!


[ users/new.html.slim ]

f:id:yukitoku_sw:20200118212441p:plain

h1 アカウント新規登録

.nav.justify-content-end
  = link_to '一覧', users_path, class: 'nav-link'

= render partial: 'form', locals: { user: @user }


[ users/edit.html.slim ]

f:id:yukitoku_sw:20200118212619p:plain

ちょっと変ですが、とりあえずね・・

h1 アカウントの編集

.nav.justify-content-end
  = link_to '一覧', users_path, class: 'nav-link'

= render partial: 'form', locals: { user: @user }

= link_to 'アカウント削除', @user[f:id:yukitoku_sw:20200118215007p:plain], method: :delete, data: { confirm: "タスク「#{@user.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger m-3'


headerにusers_pathのリンクを貼っておきます。

[ app/layouts/_header.html.slim ]

f:id:yukitoku_sw:20200118215007p:plain

ヘッダーの右端にちょこんとリンクがあります・・・

header.mb-3
  .app-title.navbar.navbar-expand-md.navbar-dark.bg-dark
    .navbar-brand TodoApp
    ul.navbar-nav.ml-auto
      li.nav-item = link_to 'User', users_path, class: 'nav-link'


ログイン機能を実装する

ControllerをGしてRoutsをdescribeする

[ コンソール ]

% rails g controller sessions new

コントローラーなのでsessionsと複数形です


[ config/routes.rb ]

Rails.application.routes.draw do

  get '/login', to: 'sessions#new'
  post '/login', to: 'sessions#create'
  delete '/logout', to: 'sessions#destroy'
  
  resources :users

  root 'tasks#index'
  resources :tasks
end

簡単に言うと、

  • getでログインのフォームを表示

  • postでログイン状態を作る

  • deleteでログイン状態を消す(ログアウト状態にする)


Viewの作成

今回はviewから作った方がわかりやすいのでviewを先に作ります

f:id:yukitoku_sw:20200118222129p:plain

ちょっと腹立つ仕様

[ app/views/sessions/new.html.slim ]

h1 ログインする?

= form_with scope: :session, local: true do |f|
  .form-group
    = f.label :email, 'メールアドレス'
    = f.text_field :email, class: 'form-control', id: 'session_email'
  .form-group
    = f.label :password, 'パスワード'
    = f.password_field :password, class: 'form-control', id: 'session_password'
  = f.submit 'ログイン', class: 'btn btn-success'

formでメールアドレスとパスワードを受け取ってユーザーを判別します


Controllerを実装する

[ app/controllers/sessions_controller.rb ]

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: session_params[:email])

    if user&.authenticate(session_params[:password])
      session[:user_id] = user.id
      redirect_to root_url, notice: 'ログインしました'
    else
      render :new
    end
  end

  def destroy
    reset_session
    redirect_to login_url, notice: 'ログアウトしました'
  end

  private

    def session_params
      params.require(:session).permit(:email, :password)
    end
end

new

viewを表示するだけなので何も記述しません。 Simple is the Best

create

まず、session_paramsから受け取ったメールアドレスに該当するユーザーを探します。
ユーザーが見つかった場合 その見つけたユーザーとsession_paramsから受け取ったパスワードを持つユーザーが同一なのか判断します。 authenticatehas_secure__passwordに付随するメソッドのことです。
引数で受け取ったパスワードをハッシュ化してUser内部に保存されているpassword_digestと一致するかを調べてくれます。一致していたらそのUserオブジェクトを返します。一致していなければfalseを返します。
user.authenticate(session_params[:password])とすると、最初のメールアドレス該当者がいなかった場合にuserがnilになります。するとauthenticateメソッドがundefined method `authenticate' for nil:NilClass (NoMethodError)が発生してしまうので&.(ボッチ演算子)を使っています。

そして、生き残ったUserがいた場合はSessionに生き残ったUserのidを格納しています。

destroy

reset_sessionをすることでsession[:user_id]から自分のuser_idを取り出すこと(ログアウト)ができます。


ログイン情報の取得をメソッド化する

現状、下記コードでログインしているユーザーを取得することができます。

User.find_by(id: session[:user_id])

これを何度も書くのは面倒です。

なので、メソッドとして定義します。

[ app/controllers/application_controller.rb ]

class ApplicationController < ActionController::Base
  helper_method :current_user

  private

    def current_user
      @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
    end
end

application_controllercurrent_userとしてhelper_methodに定義しました。 これによりcurrent_userは全てのビューで利用できます。

ヘッダーにリンクを貼って試してみます。

[ app/views/layouts/_header.html.slim ]

header.mb-3
  .app-title.navbar.navbar-expand-md.navbar-dark.bg-dark
    .navbar-brand TodoApp
    ul.navbar-nav.ml-auto
      - if current_user
        li.nav-item = link_to 'Tasks', tasks_path, class: 'nav-link'
        li.nav-item = link_to 'Users', users_path, class: 'nav-link'
        li.nav-item = link_to 'Logout', logout_path, method: :delete, class: 'nav-link'
      - else
        li.nav-item = link_to 'Login', login_path, class: 'nav-link'

ログイン前は、ログインしか表示されていない

f:id:yukitoku_sw:20200119082120p:plain

ログイン後はログイン以外が表示されています。

f:id:yukitoku_sw:20200119082202p:plain


ログインユーザーしかタスク管理ができない??

ようにします。

アカウントを登録して、ログインしないとタスク管理まで辿り着けなくするってこと!

[ app/controllers/application_controller.rb ]

class ApplicationController < ActionController::Base
  helper_method :current_user
  before_action :login_required

  private

    def current_user
      @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
    end

    def login_required
      redirect_to login_url unless current_user
    end
end

今回もapplication_controller.rbに、login_requiredってフィルタを書いていきます。

強制的にログインページに吹っ飛ばす。ログインしていなければな って感じ

しかし、このままだとログインページにもlogin_requiredが適用されて永遠に異空間を彷徨うことになってしまいます。

それは避けましょう

[ /sessions_controller.rb ]

class SessionsController < ApplicationController
  skip_before_action :login_required     #追加

  def new
  end

  (省略)
end

sessions_controllerskip_before_action :login_requiredを追加します。 これにより、セッション機能にはlogin_requiredが適用されなくなり、無事にログインページにたどり着くことができます。


ログインすれば他のユーザーのデータも扱えるクネ??

これはあかん

ログインしているユーザー "の" タスクのみ管理できるようにする

  • UserとTaskを紐付ける。

  • ログインUserに紐づいたTaskを登録する

  • ログインUserのTaskのみ一覧、詳細、変更を行えるようにする

よし!?

UserとTaskを紐付ける

% rails g migration AddUserIdToTasks

内容を編集

class AddUserIdToTasks < ActiveRecord::Migration[6.0]
  def change
    execute 'DELETE FROM tasks;'
    add_reference :tasks, :user, forign_key: true
  end
end

execで既存のTaskを削除してから、forign_keyをつけます 既存のTaskにはuser_idが設定されていないのでエラーになります。

% rails db:migrate

関連付けも必要です

[ models/user.rb ]

class User < ApplicationRecord
  has_secure_password

  validates :name, presence: true
  validates :email, presence: true, uniqueness: true

  has_many :tasks  # 追加
end

Userは複数のTaskを持っている

[ models/task.rb ]

class Task < ApplicationRecord
  validates :title, presence: true, length: { maximum: 30 }
  validates :description, presence: true

  belongs_to :user  # 追加
end

Taskは各Userに所属している


ログインUserに紐づいたTaskを登録する

tasks_controllercreateを修正します

@task = Task.new(task_params)の部分を @task = current_user.tasks.new(task_params)へと修正

これによりtaskにcurrent_userのuser_idが登録されます。 各Task毎にどのUserの所有物なのかわかるってことよ

[ app/controllers/tasks_controller.rb ]

def create
    @task = current_user.tasks.new(task_params)  # 変更箇所
    if @task.save
      redirect_to tasks_url, notice: "タスク「#{@task.title}」を登録しました。"
    else
      render :new
    end
  end


ログインUserのTaskのみ一覧、詳細、変更を行えるようにする

先ほど使った@task = current_user.tasksを乱用します

ちなみに、

@tasks = current_user.tasks

@tasks = Task.where(user_id: current_user.id)

と同じです。

[ tasks_controller.rb ]

class TasksController < ApplicationController
  def index
    @tasks = current_user.tasks
  end

  def new
    @task = Task.new
  end
  
  def create
    @task = current_user.tasks.new(task_params)
    if @task.save
      redirect_to tasks_url, notice: "タスク「#{@task.title}」を登録しました。"
    else
      render :new
    end
  end

  def show
    @task = current_user.tasks.find(params[:id])
  end

  def edit
    @task = current_user.tasks.find(params[:id])
  end

  def update
    @task = current_user.tasks.find(params[:id])
    if @task.update(task_params)
      redirect_to tasks_url, notice: "タスク「#{@task.title}」を更新しました!"
    else
      render :edit
    end
  end

  def destroy
    @task = current_user.tasks.find(params[:id])
    @task.destroy
    redirect_to tasks_url, notice: "タスク「#{@task.title}」を削除しました。"
  end

  private

    def task_params
      params.require(:task).permit(:title, :description)
    end
end

これで自分のタスクは自分でしか操作できないようになりました。


コードをわかりやすくする

ごちゃごちゃ書いたのでちょっと綺麗にしたい

tasks_controllerで乱用した@task = current_user.tasks.find(params[:id])をまとめる

[ tasks_controller.rb ]

class TasksController < ApplicationController
  before_action :set_task, only: [:show, :edit, :update, :destroy]

  def index
    @tasks = current_user.tasks
  end

  def new
    @task = Task.new
  end
  
  def create
    @task = current_user.tasks.new(task_params)
    if @task.save
      redirect_to tasks_url, notice: "タスク「#{@task.title}」を登録しました。"
    else
      render :new
    end
  end

  def show
  end

  def edit
  end

  def update
    if @task.update(task_params)
      redirect_to tasks_url, notice: "タスク「#{@task.title}」を更新しました!"
    else
      render :edit
    end
  end

  def destroy
    @task.destroy
    redirect_to tasks_url, notice: "タスク「#{@task.title}」を削除しました。"
  end

  private

    def task_params
      params.require(:task).permit(:title, :description)
    end

    def set_task
      @task = current_user.tasks.find(params[:id])
    end
end

privateの下にset_taskを定義します。中身はさっき乱用した@task = current_user.tasks.find(params[:id])
これをbefore_action :set_task, only: [:show, :edit, :update, :destroy]とすることで、指定したアクションの前に@task = current_user.tasks.find(params[:id])を実行してくれます。なので各アクションからは記述を削除してokです。

ちょっとすっきりしました。


一覧ページでの表示順を設置していなかったのでscopeを使い指定します。

[ models/task.rb ]

class Task < ApplicationRecord
  validates :title, presence: true, length: { maximum: 30 }
  validates :description, presence: true

  belongs_to :user

  scope :recent, -> { order(created_at: :desc) }  # 追加
end

登録日の新しい順に設定しました。 これをコントローラーに伝えます。

[ tasks_contorller.rb ]

 def index
    @tasks = current_user.tasks.recent
  end

これだけです!


今回はここまで・・・


_次回、機能武装


Rails 6 でTodoApp作るぜ part2

今回は、TodoAppの本体Taskモデル・Tasksコントローラー・各種ビューを作る

Taskモデル作成

テーブル設計

名称 カラム名 データ型
タイトル title string
内容 description text

めちゃめちゃシンプルに!

% rails g model Task title:string description:text

こんな感じ(nullつけ忘れました)

class CreateTasks < ActiveRecord::Migration[6.0]
  def change
    create_table :tasks do |t|
      t.string :title
      t.text :description

      t.timestamps
    end
  end
end
% rails db:migrate


nullはそのうちつける・・・

コントローラーとビュー

シンプルなCRUDってやつ!

% rails g controller tasks index show new edit

まずはRoutesを設定


[ config/routes.rb ]

Rails.application.routes.draw do

  root 'tasks#index'
  get 'tasks/index', to: 'tasks#index'
  resources :tasks
end
  • resourcesで名前付きルートやら一括設定

  • rootでroot設定だぞ!!!

  • 事件が発生した。。

tasks/indexにアクセスするとtasks/show.html.slimが表示されました。なので、get 'tasks/index', to: 'tasks#index'で押し通ります。

※ひとまず完成と言えるとこまでは止まっちゃダメ、絶対。


コントローラー

超シンプルなアプリなのでバババババ馬場馬っと書く
細かい修正は最後にやります。


[ app/controllers/tasks_controller.rb ]

class TasksController < ApplicationController
  def index
    @tasks = Task.all
  end

  def new
    @task = Task.new
  end

  def create
    @task = Task.new(task_params)
    if @task.save
      redirect_to tasks_url, notice: "タスク「#{@task.title}」を登録しました。"
    else
      render :new
    end
  end

  def show
    @task = Task.find(params[:id])
  end

  def edit
    @task = Task.find(params[:id])
  end

  def update
    @task = Task.find(params[:id])
    if @task.update(task_params)
      redirect_to tasks_url, notice: "タスク「#{@task.title}」を更新しました!"
    else
      render :edit
    end
  end

  def destroy
    @task = Task.find(params[:id])
    @task.destroy
    redirect_to tasks_url, notice: "タスク「#{@task.title}」を削除しました。"
  end

  private

    def task_params
      params.require(:task).permit(:title, :description)
    end
end

このくらいは何も見なくても書けるようになりました(^^


flashメッセージを設定したので、ちゃんと表示されるようにviewに記述します


[ app/views/layouts/application.html.slim ]

doctype html
html
  head
    title
      | TodoApp
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
  body
    .container
      - if flash.notice.present?  # 追加
        .alert.alert-success = flash.notice  # 追加
      = yield

f:id:yukitoku_sw:20200117212818p:plain

こんな感じになります。


ビューズ

こんな感じにしていきます。

f:id:yukitoku_sw:20200117210947p:plain


[ tasks/index.html.slim ]

h1 タスク一覧

.nav.justify-content-end
  = link_to '新規登録', new_task_path, class: 'nav-link'

table.table
  thead.thead-default
    tr
      th タイトル
      th 登録日
      th 編集日
      th
  tbody
    - @tasks.each do |task|
      tr
        td = link_to task.title, task
        td = task.created_at
        td = task.updated_at
        td
          = link_to '編集', edit_task_path(task), class: 'btn btn-primary mr-3'
          = link_to '削除', task, method: :delete, data: { confirm: "タスク「#{task.title}」を削除します。よろしいですか?" }, class: 'btn btn-danger'

tasks/index.html.slimでは一覧機能を表示させます。 @tasksで取得したデータをeach doで全表示させる 表示内容は、とりあえず :title:created_at`:updated_atの3つとします。

一覧ページで編集へのリンクを貼る際は、edit_task_path(task)と引数を指定しないとエラーが出る


続いて、詳細ページ

f:id:yukitoku_sw:20200117211112p:plain


[ tasks/show.html.slim ]

h1 タスクの詳細

.nav.justify-content-end
  = link_to '一覧', tasks_path, class: 'nav-link'

table.table.table-hover
  tbody
    tr
      th タイトル
      td = @task.title
    tr
      th 内容
      td = @task.description
    tr
      th 登録日
      td = @task.created_at
    tr
      th 更新日
      td = @task.updated_at

= link_to '編集', edit_task_path, class: 'btn btn-primary mr-3'
= link_to '削除', @task, method: :delete, data: { confirm: "タスク「#{@task.title}」を削除します。よろしいですか?" }, class: 'btn btn-danger'

showページでは、内容(:description)も表示されるようにする また、show では@task = Task.find(params[:id])でデータを取得しているため、編集へのリンクに引数がいらない 逆に削除へのリンクではtaskではなく@taskにする


次は_form.html.slimを作成する


[ app/views/tasks/_form.html.slim ]

= form_with model: task, local: true do |f|
  .form-group
    = f.label :title, 'タイトル'
    = f.text_field :title, class: 'form-control', id: 'task_title'
  .form-group
    = f.label :description, '詳細'
    = f.text_area :description, class: 'form-control', id: 'task_description'
  = f.submit '登録', class: 'btn btn-success'

この書き方も暗記ですね。何も見なくても書けるようにします。 コントローラで設定した、task_paramsと内容を合わせることを忘れずに!

[ tasks_controller.rb ]

(省略)
 private

    def task_params
      params.require(:task).permit(:title, :description)
    end
end

tasks_paramsの中のpermit(:title, :description)で設定してるカラムしか保存されない


そして、newとeditを実装する


f:id:yukitoku_sw:20200117211234p:plain

[ tasks/new.html.index ]

h1 タスク新規登録

.nav.justify-content-end
  = link_to '一覧', tasks_path, class: 'nav-link'

= render partial: 'form', locals: { task: @task }


f:id:yukitoku_sw:20200117211322p:plain


[ tasks/edit.html.index ]

h1 タスクの編集

.nav.justify-content-end
  = link_to '一覧', tasks_path, class: 'nav-link'

= render partial: 'form', locals: { task: @task }

ここでは、= render 'form'とはせずに、= render partial: 'form', locals: { task: @task }と記述しています。
こうすると「インスタンス変数@taskを、パーシャル内のローカル変数taskとして渡す」ことができます。
= render 'form'としたい場合は、

[ app/views/tasks/_form.html.slim ]

= form_with model: @task, local: true do |f|  #@taskに変更する
  .form-group
    = f.label :title, 'タイトル'
    = f.text_field :title, class: 'form-control', id: 'task_title'
  .form-group
    = f.label :description, '詳細'
    = f.text_area :description, class: 'form-control', id: 'task_description'
  = f.submit '登録', class: 'btn btn-success'

model: taskmodel: @taskにする必要があります。

以上でシンプルなCRUD機能の完成です。


ヘッダーの作成

ちょっと寂しいので簡単なヘッダーを作ります

ファイルapp/views/layouts/_headerを作成します。


[ layouts/_header ]

header.mb-3
  .app-title.navbar.navbar-expand-md.navbar-dark.bg-dark
    .navbar-brand TodoApp


そして、application.html.slimのbodyの下(.containerの上)にrenderします。

[ layouts/application.html.slim ]

doctype html
html
  head
    title
      | TodoApp
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
  body
    = render 'layouts/header'  # 追加
    .container
      - if flash.notice.present?
        .alert.alert-success = flash.notice
      = yield


f:id:yukitoku_sw:20200117212320p:plain

ひとまず、よしっ

モデルに検証機能を加える

今のままだと、新規登録画面で何も書かなくても登録できてしまいます。それだと困る・・ことは現状ないですが、実際にwebサービスとして運用するとなるとあまりよろしくありません。
なので:title:descriptionには何も入力されていなければエラーになるように設定します。

[ app/models/task.rb ]

class Task < ApplicationRecord
  validates :title, presence: true, length: { maximum: 30 }
  validates :description, presence: true
end

presence: trueとすることで入力必須項目となり、エラーを発生させることができます。
titleの方にはlength: { maximum: 30 }も気分でつけてみました。これにより、タイトルは30文字以内にしないとエラーが出るようになります。このTodoAppは私の思うがままです。(笑)


このままでは、コンソールを開かないとなんのエラーが出たのかわからないのでviewで表示されるように設定します。

[ app/views/tasks/_form.html.slim ]

- if task.errors.present?
  ul#error_explanation
    - task.errors.full_messages.each do |message|
      li = message
      
= form_with model: task, local: true do |f|
  .form-group
    = f.label :title, 'タイトル'
    = f.text_field :title, class: 'form-control', id: 'task_title'
  .form-group
    = f.label :description, '詳細'
    = f.text_area :description, class: 'form-control', id: 'task_description'
  = f.submit '登録', class: 'btn btn-success'

formに入力された値が正しくなければエラーを出す。ということで_form.html.slimにブチ込みました。

もしエラーがあれば、それらのエラーをあるだけ全部伝えてくれ!
という内容ですね。


今回はここまで。


次回 User作る