gotagota日記

「面白きことは良きことなり」

ドットインストール Rails まとめ

ドットインストールさんで Rails について振り返ったので、勉強メモ。

Rails は前にちょこっとやった時は、ただコードを書き写していただけに等しかったのですが、今回、RubyやらActiveRecord やらを勉強しなおした後にやると理解、納得しながらできたので得るものは大きかったです。

これは、自分で何もみずに同じようなアプリを作るという試みの際の解答のようなものになればと思い残しておきます。

[1] 事前準備

実行環境

自分の実行環境は以下のとおり。

補足

gem に関しては全て Bundler を使って入れます。
詳しくは rbenv と bundler で rails 環境構築メモ に書きましたので宜しければ。
あと、毎回 bundle exec と打つのがめんどかったので be というエイリアスを設定しています。適宜読み替えてください。

手順

まずは事前準備をしていきます。

1. rails new してフォルダに移動

  • $ be rails new taskapp
  • $ cd taskapp

2. Gemfile を編集

  • gem 'therubyracer', platforms: :ruby のコメントを外す

3. gem をインストールする

  • $ bundle install

[2] Project に関するあれやこれ

タスク管理アプリということで、プロジェクト毎にタスクがあると思うので、まずは大本となるプロジェクト部分に関するあれやこれを作っていきます。

以下、実際の手順。

手順

1. Model と Controller を用意する

  • $ be rails g model Project title:string で Model を作成
    • Model の名前は単数形で最初を大文字にする必要がある
    • データ型のデフォルトは文字列なので :string は省略可能
  • $ be rake db:migrate でデータベースを作る。
  • $ be rails g controller Projects でController を作成
    • Controller の名前は複数形で最初を大文字にする必要がある

2. ルーティングをいじる

  • config/routes.rb を編集
Rails.application.routes.draw do
  resources :projects
  root 'projects#index'
end

3. View を作る

  • app/views/projects/index.html.erb を作成する
  • 現時点で中身は空でもおk

4. 共通点プレートをいじる

① 共通ロゴと共通リンクを設定する
  • app/assets/images に 適当な logo.png ファイルを入れておく
  • app/views/layouts/application.html.erb を編集
<!DOCTYPE html>
<html>
<head>
  <title>Taskapp2</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>
<%= image_tag "logo.png" %>
<%= yield %>
<%= link_to "Home", projects_path %>
</body>
</html>
  • image_tag ヘルパーと link_to ヘルパーを利用して上記のように書く
  • projects_path$ be rake routes でルーティングを確認した際の左端( Prefix カラム) に _path をつけたもの
② 共通スタイルシートを設定する
  • app/assets/stylesheets/application.css をいじる
  body {background-color: #eee;}

5. 新規作成フォームを作成し、データを保存し、表示する

app/controllers/projects_controller.rb をいじる
class ProjectsController < ApplicationController
  def index
    @projects = Project.all
  end
  
  def show 
    @project = Project.find(params[:id])
  end
  
  def new
    @project = Project.new
  end
  
  def create
    @project = Project.new(project_params)
    @project.save
    redirect_to projects_path
  end
  
  private
    def project_params
      params[:project].permit(:title)
    end
end
  • private 以下はセキュリティ対策のためのもの。
app/views/projects/new.html.erb を作成する
<h1>Add New</h1>
<%= form_for @project do |f| %>
<p>
  <%= f.label :title %><br>
  <%= f.text_field :title %>
</p>
<p>
  <%= f.submit %>
</p>
<% end %>
  • form_for ヘルパーを利用する
  • submit したものが Projects#create に送られる
app/views/layouts/application.html.erb をいじる
<!DOCTYPE html>
<html>
<head>
  <title>Taskapp2</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>
<%= image_tag "logo.png" %>
<%= yield %>
<%= link_to "Add New", new_project_path %> |
<%= link_to "Home", projects_path %>
</body>
</html>
app/views/projects/index.html.erb を編集する
<h1>Projects</h1>

<ul>
  <% @projects.each do |project| %>
  <li>
    <%= link_to project.title, project_path(project.id) %>
  </li>
  <% end %>
</ul>
app/views/projects/show.html.erb を作成する
<h1><%= @project.title %></h1>

6. Validation を設定してエラーメッセージを表示する

  • 今ままでは空の状態でも新規登録できてしまうので、 app/models/project.rb を編集
class Project < ActiveRecord::Base
  validates :title, presence: true
end
  • 今の設定のままだと、Validation に引っかかった時でも、 プロジェクト一覧にリダイレクトしてしまうので、 app/controllers/projects_controller.rb の create アクション を次のように編集
  def create
    @project = Project.new(project_params)
    if @project.save
      redirect_to projects_path
    else
      render 'new'
    end
  end
  • エラーメッセージを表示するとより親切なので app/views/projects/new.html.erb を編集していく
<h1>Add New</h1>

<%= form_for @project do |f| %>

<p>
  <%= f.label :title %><br>
  <%= f.text_field :title %>
  <% if @project.errors.any? %>
  <%= @project.errors.messages[:title][0] %>
  <% end %>
</p>

<p>
  <%= f.submit %>
</p>

<% end %>
  • エラーが発生している場合、 @project.errors にエラーメッセージが入るので、 <% if @project.errors.any? %> で条件分岐をかけて、メッセージを表示させている。

  • また、エラーメッセージ、バリデーションルールは任意のものに設定できる。 app/models/project.rb を編集

class Project < ActiveRecord::Base
  validates :title,
  presence: {message: "入力してください"},
  length: {minimum: 3, message: "短すぎ!"}
end

7. 編集フォームを作ってデータを更新する

  • まずは編集フォームを作っていく。 app/views/projects/edit.html.erb を作成。といっても中身は <h1> タグだけ変えて、あとは app/views/projects/new.html.erb と一緒。
<h1>Edit</h1>

<%= form_for @project do |f| %>

<p>
  <%= f.label :title %><br>
  <%= f.text_field :title %>
  <% if @project.errors.any? %>
  <%= @project.errors.messages[:title][0] %>
  <% end %>
</p>

<p>
  <%= f.submit %>
</p>

<% end %>
  • 次に編集画面へのリンクを追加する。 app/views/projects/index.html.erb を編集
<h1>Projects</h1>

<ul>
  <% @projects.each do |project| %>
  <li>
    <%= link_to project.title, project_path(project.id) %>
    <%= link_to "[Edit]", edit_project_path(project.id) %>
  </li>
  <% end %>
</ul>
  • アクションをまだ作っていないので、 app/controllers/projects_controller.rb に edit アクションを追加
  def edit
    @project = Project.find(params[:id])
  end
  • この時点で編集画面へジャンプできるが、update アクションをまだ作っていないので、更新できない。 app/controllers/projects_controller.rb に update アクションを追加
  def update
    @project = Project.find(params[:id])
    if @project.update(project_params)
      redirect_to projects_path
    else
      render 'edit'
    end
  end

8. データを削除する

  • app/views/projects/index.html.erb を編集し、Delete リンクを追加する。 data: {confirm: "are you sure?"} と記述することで確認アラートを出すことができる。
<h1>Projects</h1>

<ul>
  <% @projects.each do |project| %>
  <li>
    <%= link_to project.title, project_path(project.id) %>
    <%= link_to "[Edit]", edit_project_path(project.id) %>
    <%= link_to "[Delete]", project_path(project.id), method: :delete, data: {confirm: "are you sure?"} %>
  </li>
  <% end %>
</ul>
  • destroy アクションを追加する。 app/controllers/projects_controller.rb に追加。
  def destroy
    @project = Project.find(params[:id])
    @project.destroy
    redirect_to projects_path
  end

9. 重複するコードを共通化し、 Dry を目指す

  • まずフォームで重複する部分があるので、共通化していく。

  • app/views/projects/new.html.erbapp/views/projects/edit.html.erb の共通部分をカットし、 <%= render 'form' %> で置き換える。

<h1>Add New</h1>
<%= render 'form' %>
<h1>Edit</h1>
<%= render 'form' %>
  • app/views/projects/_form.html.erb を作成し、共通部分をコピペ。
<%= form_for @project do |f| %>
<p>
  <%= f.label :title %><br>
  <%= f.text_field :title %>
  <% if @project.errors.any? %>
  <%= @project.errors.messages[:title][0] %>
  <% end %>
</p>
<p>
  <%= f.submit %>
</p>
<% end %>
  • 次に、Controller でも重複する部分があるので、共通化していく。

  • @project = Project.find(params[:id]) が大量に含まれるため、 before_action でまとめる。その際、set_project 関数を private に用意すると後々編集する際に便利。

class ProjectsController < ApplicationController

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

  def index
    @projects = Project.all
  end

  def show
  end
  
  
  (中略)
  
  
  private

    def project_params
      params[:project].permit(:title)
    end

    def set_project
      @project = Project.find(params[:id])
    end
end

[3] Task に関するあれやこれ

次に、プロジェクトに属するタスクを設定するあれやこれを作っていきます。

以下、実際の手順。

手順

1. Model と Controller を用意する

  • $ be rails g model Task title done:boolean project:references で Model を作る
    • 繰り返しになるが、モデル名は単数形、 stirng 型は省略可能
    • booelan は真偽値型で、 project:references は Project モデルと紐付けろ、という意味
    • タスクが終わっているかどうかという項目の done はデフォルトで 未完了を表す false であるのが望ましいので、 db/migrate/**********_create_tasks.rb を以下のように修正。
class CreateTasks < ActiveRecord::Migration
  def change
    create_table :tasks do |t|
      t.string :title
      t.boolean :done, default: false
      t.references :project, index: true

      t.timestamps
    end
  end
end
  • $ be rake db:migrate でデータベースを作る

  • $ be rails g controller Tasks で Controller を作る

2. Association を設定する

  • 先ほど Task モデルを作った際には Project モデルとの紐付けオプションをつけて実行したが、 Project モデルを作った際にそのようなオプションはつけなかったので手動で設定する必要がある。
  • app/models/project.rb を編集。
class Project < ActiveRecord::Base
  has_many :tasks
  validates :title,
  presence: {message: "入力してください"},
  length: {minimum: 3, message: "短すぎ!"}
end
  • これで、「一対多の関係」、つまり、「1つの Project に対して、複数の Task」 という関係ができた。

  • 続いて、Task に関するルーティングをいじっていく。とりあえず、 create アクションと、 destroy アクションができればいいので以下のように、 config/routes.rb を編集する

Rails.application.routes.draw do
  resources :projects do
    resources :tasks, only: [:create, :destroy]
  end
  root 'projects#index'
end

3. 新規作成フォームを作り、 task を保存する

  • task の新規作成フォームを作るために、 app/views/projects/show.html.erb を編集する
<h1><%= @project.title %></h1>
<u1>
<% @project.tasks.each do |task| %>
<li><%= task.title %></li>
<% end %>
<li>
  <%= form_for [@project, @project.tasks.build] do |f| %>
  <%= f.text_field :title %>
  <%= f.submit %>
  <% end %>
</li>
</u1>
  • create アクションがまだないので、app/controllers/tasks_controller.rb に追加
class TasksController < ApplicationController

  def create
    @project = Project.find(params[:project_id])
    @task = @project.tasks.create(task_params)
    redirect_to project_path(@project.id)
  end

  private

    def task_params
      params[:task].permit(:title)
    end
end
  • 空の task を弾くためのバリデーションを設定する。 app/models/task.rb を編集
class Task < ActiveRecord::Base
  belongs_to :project
  validates :title, presence: true
end

4. Task を削除する

  • Delete リンクを追加するために、app/views/projects/show.html.erb を編集する
<h1><%= @project.title %></h1>
<u1>
  <% @project.tasks.each do |task| %>
  <li>
    <%= task.title %>
    <%= link_to "[Delete]", project_task_path(task.project_id, task.id), method: :delete, data: {confirm: "are you sure?"} %>
  </li>
  <% end %>
  <li>
  <%= form_for [@project, @project.tasks.build] do |f| %>
  <%= f.text_field :title %>
  <%= f.submit %>
  <% end %>
  </li>
</u1>
  • destroy アクションをまだ作っていないので、 app/controllers/tasks_controller.rb に追加
  def destroy
    @task = Task.find(params[:id])
    @task.destroy
    redirect_to project_path(params[:project_id])
  end

5. チェックボックスを用意して done を切り替える

  • app/views/projects/show.html.erb のリストの中のタスクタイトルの前にチェックボックスを追加。
  • check_box_tag ヘルパーを利用。最初の引数には id を、2番目の引数には value を、3番目の引数にはチェックされているかどうかの true/false をそれぞれ指定できる。
  • その後ろには、 HTML に渡したい属性をいくらでも追加できる。今回は、後で jQuery で使いたいので、扱いやすいように task.idproject_iddata-iddata-project-id という属性で管理できるようにした。
  • 下のコードを追記してから、ブラウザでソースを見ると、 <input data-id="…" data-project_id="…" id="" name="" type="checkbox" value="" />というのが出てきており、data-id 属性と data-project_id 属性で Task の id と Project の id が管理できているのがわかる。
<h1><%= @project.title %></h1>
<u1>
  <% @project.tasks.each do |task| %>
  <li>
    <%= check_box_tag '', '', task.done, {'data-id' => task.id, 'data-project_id' => task.project_id} %>
    <%= task.title %>
    <%= link_to "[Delete]", project_task_path(task.project_id, task.id), method: :delete, data: {confirm: "are you sure?"} %>
  </li>
  <% end %>
  <li>
  <%= form_for [@project, @project.tasks.build] do |f| %>
  <%= f.text_field :title %>
  <%= f.submit %>
  <% end %>
  </li>
</u1>
  • 次に、チェックボックスが切り替わるたびに、 POST を送りたい。そのための toggle アクションを jQuery を用いつつ、用意する。 app/views/projects/show.html.erb の一番下に以下のスクリプトを追加する。
  • このスクリプトの意味は、「チェックボックスがクリックされた際に、 Ajax/projects/xxx/tasks/xxx/toggle に POST しなさい」という意味。
<script>
  $(function(){
    $("input[type=checkbox]").click(function(){
      $.post('/projects/'+$(this).data('project_id')+'/tasks/'+$(this).data('id')+'/toggle');
    });
  })
</script>
  • POST のルーティングが未設定なので config/routes.rb で設定する。以下を追記。
post '/projects/:project_id/tasks/:id/toggle' => 'tasks#toggle'
  • POST 先の toggle アクションがないので作る。 app/controllers/tasks_controller.rb に追加。また、今回は、 toggle アクションリクエストした際に、テンプレートを使わないので、その旨も書く。
  def toggle
    @task = Task.find(params[:id])
    @task.done = !@task.done
    @task.save
    render nothing: true
  end
  • 以上で、タスクの完了/未完了がページをまたいでも保持されるようになる。

6. Tasks の数を表示する

  • まず残タスク数を表示したいので、「 donefalse のものを検索条件として保存しなさい」という意味で、 app/models/tasks.rb に以下を追記。
  • これで :unfinished 関数ができるので、どこからでも残タスク数にアクセスできる。
  scope :unfinished, -> {where(done: false)}
  • 実際に表示するために、app/views/projects/index.html.erb を以下のように書き換える。タイトル部分に先ほど定義した :unfinished 関数 を用いて、残タスク数を表示している。
<h1>Projects</h1>

<ul>
  <% @projects.each do |project| %>
  <li>
    <%= link_to project.title, project_path(project.id) %> (<%= project.tasks.unfinished.count %>/<%= project.tasks.count %>)
    <%= link_to "[Edit]", edit_project_path(project.id) %>
    <%= link_to "[Delete]", project_path(project.id), method: :delete, data: {confirm: "are you sure?"} %>
  </li>
  <% end %>
</ul>