いまさら聞けないRuby on Railsとherokuでつくるシンプルなタスク管理ツール #1

2012.11.05

前回まではRubyの開発環境を整えましたので、今回はRuby on Railsを使ってサクッとタスク管理ツールを作成して、次回herokuにデプロイするとこまでいきたいと思います。
ちなみに開発はMac OS Xで進めていきます。

はじめに

Ruby on RailsとはRubyで作成されたオープンソースのWeb Application Frameworkです。
MVCアーキテクチャを採用していて、「同じことを繰り返さない/DRY:Don't Repeat Yourself」と「設定より規約/CoC:Convention over Configuration」の基本理念に基づくことで、より少ないコードで高速な開発が可能になります。

Railsアプリ作成する

さてここからRailsアプリを作成していきます。
まずは作業ディレクトリに移動してアプリを格納するフォルダを作成しましょう。

$ mkdir my-simple-todos

ここでつけるフォルダ名に注意してください。今回のようにmy-simple-todosとした場合、MySimpleTodosというネームスペースでRailsアプリが作成されます。

次にGemfileを作成してRailsをインストールします。

$ cd my-simple-todos
$ bundle init
Writing new Gemfile to /Users/yamagata.kozo/tmp/my-simple-todos/Gemfile
$ vim Gemfile
# A sample Gemfile
source "https://rubygems.org"

# gem "rails"

親切にもGemfileにはrailsが最初から書いてありますのでコメントを外しておくだけでOKです。
保存したら以下のコマンドでインストールを実行してください。

$ bundle install --path vendor/bundle --without production 

これでgemはすべてvendor/bundleフォルダにインストールされ、production環境、つまりheroku環境に必要なgemを無視することができます。
gemのインストールが終了したら以下を実行してください。

$ bundle exec rails new . --skip-bundle
 exist create README.rdoc
create Rakefile
create config.ru
create .gitignore
conflict Gemfile Overwrite /Users/yamagata.kozo/tmp/my-simple-todos/Gemfile? (enter "h" for help) 「Ynaqdh」

途中で何か質問されました。事前に用意したGemfileに設定を上書きしても良いかという確認ですので気にせずそのままEnterを押してください。
これでRailsアプリのひな形が生成されたはずです。続いてもう一度Gemfileを開いて以下のようにgemを追加してください。

source 'https://rubygems.org'

gem 'rails', '3.2.8'

# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'

group :development, :test do
  gem 'sqlite3'
end

group :production do
  gem 'pg'
  gem 'therubyracer-heroku'
  gem 'thin'
end
# ... 以下省略 ...

ハイライトした行が追加の記述です。ローカルの開発環境ではDBにsqlite3を、herokuではpostgreSQLを使用します。また、CoffeeScriptのminify等にJavaScriptの実行環境が必要ですのでheroku用にカスタマイズされたtherubyracer-herokuを追加し、アプリのサーバーにthinを使用します。
gemの追加が終わったらbundle installを実行してください。

$ bundle install
Fetching gem metadata from https://rubygems.org/...........
Fetching gem metadata from https://rubygems.org/..
Using rake (0.9.2.2) 
Using i18n (0.6.1)
Using multi_json (1.3.6)
... 以下省略 ...

これで必要なgemが全てインストールされ開発の準備が整いました。
試しにサーバを起動してみましょう。

$ bundle exec rails s
=> Booting WEBrick
=> Rails 3.2.8 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2012-11-05 10:29:27] INFO WEBrick 1.3.1
[2012-11-05 10:29:27] INFO ruby 1.9.3 (2012-10-12) [x86_64-darwin11.4.2]
[2012-11-05 10:29:27] INFO WEBrick::HTTPServer#start: pid=2926 port=3000

アプリケーションのディレクトリで上記のコマンドを実行するとlocalhost:3000で開発用サーバが起動します。ブラウザで開けばWelcome aboardというページが表示されるはずです。
(sというコマンドはserverの略です。serverと打つこともできます)

gitリポジトリを作成しておく

アプリの準備が整ったらgitも使えるようにしておきましょう。git initでリポジトリを作成してrailsが作成した.gitignoreファイルに記述を追加します。

$ git init
$ vim .gitignore
# See http://help.github.com/ignore-files/ for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile ~/.gitignore_global

# Ignore bundler config
/.bundle

# Ignore the default SQLite database.
/db/*.sqlite3

# Ignore all logfiles and tempfiles.
/log/*.log
/tmp

.DS_Store
vendor/bundle

最初のコミットも実行しておきましょう。

$ git add .
$ git commit -m "Initial commit"

機能を実装する

ここからはガンガン機能を実装していきます。と言ってもscaffoldを使えば大体の実装が済んでしまいますが…
今回はタスク管理ツールですので、タスク名、終了時刻を記録出来るようにしておきます。
Ctrl + c でサーバを終了して以下のコマンドを実行してください。

$ bundle exec rails g scaffold task name:string finished_at:datetime
invoke active_record
  create db/migrate/20121105013846_create_tasks.rb
  create app/models/task.rb
invoke test_unit
  create test/unit/task_test.rb
  create test/fixtures/tasks.yml
 ... 以下略 ...

何やらたくさんのファイルが作成されました。これらは単にファイルを作成しただけではなくコードも既に記述されています。またrailsにはmigrationという機能があり、rubyスクリプトでDBの状態を管理できるようになります。
タスク情報のテーブルを作成するmigrationファイルも先ほど生成されていますので、以下のコマンドを実行してください。

$ bundle exec rake db:migrate
== CreateTasks: migrating ====================================================
-- create_table(:tasks) -> 0.0015s
== CreateTasks: migrated (0.0016s) ===========================================

これでtasksテーブルが作成されました。
サーバを起動し http://localhost:3000/tasks にブラウザでアクセスするとタスクのCRUD(作成、閲覧、編集、削除)が可能になっているはずです。

動作に問題なければgit commitしておきます。

$ git add .
$ git commit -m "Generate tasks"

タスク管理ツールらしくする

ここまででも一応タスクを登録できますが、状態を終了にできたほうが便利です。このあたりの機能を実装してみたいと思います。
まずはTaskモデルを編集します。

app/models/task.rb

class Task < ActiveRecord::Base
  attr_accessible :name
  
  default_scope order('created_at DESC')
  
  def finish!
    self.finished_at = Time.now
	save!
  end
  
  def unfinish!
    self.finished_at = nil
	save!
  end
  
  def finished?
    self.finished_at.presence
  end
end

finished_atカラムは直接弄らないようにしますので、attr_accessibleから :finished_at を削除しておきました。
またデータを取得する際、作成日の新しいものが先頭に来るようにdefault_scopeをあてて、
さらにfinish!、unfinish!、finished?というメソッドを追加しました。それぞれ終了状態の変更と確認をfinished_atカラムを使って行なっています。

次はviewを編集します。

app/views/tasks/index.html.erb

<h1>Listing tasks</h1>

<table>
  <tr>
    <th>Name</th>
    <th>Finished at</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @tasks.each do |task| %>
  <tr>
    <td><%= task.finished? ? '[FINISHED]' : nil %><%= task.name %></td>
    <td><%= task.finished_at %></td>
    <td><%= link_to 'Show', task %></td>
    <td><%= link_to 'Edit', edit_task_path(task) %></td>
    <td>
      <% if task.finished? %>
        <%= link_to 'Unfinish', '#' %>
      <% else %>
        <%= link_to 'Finish', '#' %>
      <% end %>
    </td>
    <td><%= link_to 'Delete', task, data: { method: 'delete' } %></td>
  </tr>
<% end %>
</table>

完了しているものが分かりやすくするため頭に[FINISHED]をつけるようにして、Finish、Unfinishリンクも状態によって出し分けられるようにしました。リンクURLはとりあえず#で構いません。

また登録・編集のフォームからfinished_atのフィールドを削除しておきます。

app/views/tasks/_form.html.erb

<%= form_for(@task) do |f| %>
  <% if @task.errors.any? %>
    <div id="error_explanation">
      <h2 id="toc-prohibited-this-task-from-being-saved"><%= pluralize(@task.errors.count, "error") %> prohibited this task from being saved:</h2>

      <ul>
      <% @task.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :finished_at %><br />
    <%= f.text_field :finished_at %>
  </div>  
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

ハイライトした行は削除しておいてください。

状態を切り替えるコントローラが必要

状態の切り替えメソッドと、閲覧は出来るようになりましたが肝心の切り替えがまだ出来ません。
TasksControllerにアクションを追加してもいいのですが、今回は専用のコントローラーで実装してみようと思います。どう実装するかは好みが別れるところだと思います。

コントローラーの作成は以下のコマンドを実行してください。

$ bundle exec rails g controller tasks/finishes create destroy
create app/controllers/tasks/finishes_controller.rb
route get "finishes/destroy"
route get "finishes/create"
... 以下略 ...

これで create と destroy アクションを備えたTasks::FinishesControllerが作成されました。
次はこのコントローラにURLを紐付けていきます。

config/routes.rb

MySimpleTodos::Application.routes.draw do
  resources :tasks do
    resource :finish, :only => [:create, :destroy], :module => :tasks
  end
... 以下略 ...

コントローラーを作成したときに自動で追加された get "finishes/create" などは必要ありませんの削除しておいてください。代わりにtasksリソースの記述にブロックを渡して単一リソースとして :finish を渡しておきます。またアクションは create と destroy しかありませんので only オプションに配列で指定しておきます。moduleはtasksフォルダにコントローラが格納されているので必要になります。

ではroutes.rbで追加したrouteをコマンドで確認してみましょう。

$ bundle exec rake routes
task_finish POST   /tasks/:task_id/finish(.:format) tasks/finishes#create
            DELETE /tasks/:task_id/finish(.:format) tasks/finishes#destroy
      tasks GET    /tasks(.:format)                 tasks#index
... 以下略 ...

ちゃんとTasksコントローラに加えてTasks::FinishesControllerのrouteも追加されています。
create も destroy も同じパスですがHTTPリクエストメソッドが違うところに注目してください。

ここまで来ればあともう少しです。
rake routesで確認したリストの一番左にrouteの名称が書かれていますので、この task_finish の末尾に _path を加えると、該当のパスを生成するヘルパーメソッドとして使えるようになります。これを先ほど編集したviewに埋め込んで行きます。

app/views/tasks/index.html.erb

... 以上略 ...
      <% if task.finished? %>
        <%= link_to 'Unfinish', task_finish_path(task), data: { method: 'delete' } %>
      <% else %>
        <%= link_to 'Finish', task_finish_path(task), data: { method: 'post' } %>
      <% end %>
... 以下略 ...

仮で#にしておいた部分に追加しています。引数としてtaskオブジェクトを渡すことで該当のidを抽出してパスに組み込んでくれるようになります。またdataオプションにmethodを指定したハッシュを渡すことでhtmlのCustom data attributesを利用してリクエストメソッドをGET以外も使えるようになります。(JavaScriptで制御していますので、オフにされている環境でも動くようにbutton_toというヘルパーもあります)

ではコントローラーの中身を実装しましょう。

app/controllers/tasks/finishes_controller.rb

class Tasks::FinishesController < ApplicationController
  # POST /tasks/1/finish
  def create
    @task = Task.find(params[:task_id])
	@task.finish!
	redirect_to @task, notice: 'Task was finished'
  end
  
  # DELETE /tasks/1/finish
  def destroy
    @task = Task.find(params[:task_id])
	@task.unfinish!
	redirect_to @task, notice: 'Task was unfinished'
  end
end

状態を変更したいtaskデータを見つけて変更し、個別ページヘリダイレクトしています。noticeオプションでメッセージも付けておきました。

以上で実装は終了です。サーバーを起動して動作を確認してみてください。railsコマンドを使って割りと少ない記述でサクサクと実装が進められました。規約に従って得られる恩恵はモデルの関連付けなどを実装する場合に特に感じられると思います。

では次回はherokuにデプロイして使えるようにしてみたいと思います。