MVC of Redmine Plugin in Bootstrap

==================================

[前回](http://qiita.com/items/4f77f851e3936ef81a69)はほぼScaffoldのみでRedmine Plugin(Redmine Animal)を作りました。
今回はRailsで開発の中心になるModel - View - Controller (MVC)を紹介します。

Scaffoldで出力されたファイルを修正してシンプルなデータのメンテナンス機能を実装します。

# Model

まずはモデルです。データの操作とビジネスロジックを記述します。
データ操作は`ActiveRecord::Base`を継承しているのでO/Rマッピングが利用できます。
また、モデルレベルでデータを検証するには[validates](http://railsdoc.com/validation)を利用します。

`AnimalCat `クラスは`name`を必須入力にます。さらに値の一意を検証します。
`age`は値が`0`から`99`の範囲に含まれているか検証します。
(Railsは`validates presence of name.`のようにメソッドで英文になります。)

インスタンスメソッドの`is_kitten?`は1歳未満を子猫と判断します。
(Rubyはメソッドで最後に評価した式の結果を返します。)

```ruby:plugins/redmine_animal/app/models/animal_cat.rb
class AnimalCat < ActiveRecord::Base
  unloadable

  validates_presence_of :name
  validates_uniqueness_of :name, case_sensitive: false
  validates_inclusion_of :age, in: 0..99

  def is_kitten?
    age < 1
  end
end
```

`rails server`コマンドはHTTPサーバでWebアプリケーションが起動しましたが
`rails console`コマンドはirb(Interactive Ruby)のコンソールで起動します。
モデルクラスからデータを操作したりビジネスロジックの実行などをします。  

```bash
$ bundle exec rails console
Loading development environment (Rails 3.2.13)

irb(main):001:0> AnimalCat.all
  AnimalCat Load (0.2ms)  SELECT "animal_cats".* FROM "animal_cats" 
=> [#<AnimalCat id: 1, name: "もも", age: 2>, #<AnimalCat id: 2, name: "クロ", age: 0>]

irb(main):002:0> AnimalCat.all.first
  AnimalCat Load (0.3ms)  SELECT "animal_cats".* FROM "animal_cats" 
=> #<AnimalCat id: 1, name: "もも", age: 2>

irb(main):003:0> AnimalCat.all.first.is_kitten?
  AnimalCat Load (0.2ms)  SELECT "animal_cats".* FROM "animal_cats" 
=> false

irb(main):004:0> AnimalCat.all.last
  AnimalCat Load (0.2ms)  SELECT "animal_cats".* FROM "animal_cats" 
=> #<AnimalCat id: 2, name: "クロ", age: 0>

irb(main):005:0> AnimalCat.all.last.is_kitten?
  AnimalCat Load (0.2ms)  SELECT "animal_cats".* FROM "animal_cats" 
=> true
```

`AnimalCat.all`で`SELECT "animal_cats".* FROM "animal_cats"`のクエリーが実施されます。
`AnimalCat.all.first`はメソッドチェーンの仕組みで先頭のレコードを取得します。
(`もも`は2歳なので子猫ではありません。`クロ`は0歳なので子猫です。)


# View

次はビューです。レスポンスのテンプレートを記述します。
テンプレートエンジンは[erb](http://jp.rubyist.net/magazine/?0017-BundledLibraries)の他に[haml](http://haml.info/)や[slim](http://slim-lang.com/)などがあります。

## Layout

HTMLは`[resource]`の`[action].html.erb`に`<body>`タグを記述します。
`<html>`タグは`layouts`の`application.html.erb`に記述します。

```
. 
└─app
    └─views
        ├─[resource]
        │      [action].html.erb
        │      
        └─layouts
                application.html.erb
```

レイアウトはコントローラやアクションで変更することができます。

つまりRedmineのレイアウトを使わないでデザインすることが可能です。
さっそくAnimal Pluginのレイアウトを`animal.html.erb`ファイルに作ります。

```erb:plugins/redmine_animal/app/views/layouts/animal.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>Animal</title>
</head>
<body>

<%= yield %>

</body>
</html>
```

他のリソースにもレイアウトを利用するケースがあるので`ApplicationController`を
継承した`AnimalBaseController`を`animal_base_controller.rb`ファイルに作ります。

```ruby:plugins/redmine_animal/app/controllers/animal_base_controller.rb
class AnimalBaseController < ApplicationController
  unloadable
  layout 'animal'
end
```

`AnimalCatsController`の継承を`ApplicationController`から`AnimalBaseController`に変更します。

```ruby:plugins/redmine_animal/app/controllers/animal_cats_controller.rb
class AnimalCatsController < AnimalBaseController
...
end
```

これでスタイルのない状態になりました。スタイルはシンプルで機能的なデザインができる[Bootstrap](http://twitter.github.io/bootstrap/)を使います。

![animal_cat_layout_simple](https://dl.dropboxusercontent.com/u/14690051/images/notes/its/redmine/example/animal_cat_layout_simple.png)

Bootstrapは`Redmine Twitter Bootstrap Plugin`で簡単に使えるようになります。

```bash
$ git clone git://github.com/ogom/redmine_twitter_bootstrap ./plugins/redmine_twitter_bootstrap
```

先ほどの`animal.html.erb`ファイルにBootstrapのスタイルシートとジャバスクリプトを追加します。

```erb:plugins/redmine_animal/app/views/layouts/animal.html.erb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title><%=h html_title %></title>
<meta name="description" content="<%= Redmine::Info.app_name %>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= csrf_meta_tag %>
<%= favicon %>

<style>
  body {padding-top: 60px;}
</style>
    
<%= stylesheet_link_tag "bootstrap.min.css", plugin: :redmine_twitter_bootstrap %>
<%= stylesheet_link_tag "bootstrap-responsive.min.css", plugin: :redmine_twitter_bootstrap %>

<%= javascript_heads %>
<%= javascript_include_tag "bootstrap.min.js", plugin: :redmine_twitter_bootstrap %>
</head>

<body data-spy="scroll">

<div class="navbar navbar-inverse navbar-fixed-top">
  <div class="navbar-inner">
    <div class="container-fluid">
      <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="brand" href="/"><%= page_header_title %></a>
      <div class="nav-collapse collapse">
        <p class="navbar-text pull-right">
          Logged in as <%= "#{link_to_user(User.current, format: :username)}".html_safe if User.current.logged? %>
        </p>
        <ul class="nav">
          <%= nav_list_tag("Cats", {controller: "animal_cats", action: "index"}) %>
          <li><a href="/animal/dogs">Dogs</a></li>
        </ul>
      </div>
    </div>
  </div>
</div>

<div class="container">
  <div class="content-fluid">  

<%= yield %>

  </div>
</div>

</body>
</html>
```

BootstrapのHelperを`AnimalBaseHelper`にインクルードします。

```ruby:plugins/redmine_animal/app/helpers/animal_base_helper.rb
module AnimalBaseHelper
  include RedmineTwitterBootstrap::Helper
end
```

ナビバーが表示されるようになりました。

![animal_cat_layout_tb](https://dl.dropboxusercontent.com/u/14690051/images/notes/its/redmine/example/animal_cat_layout_tb.png)

他のファイルにもBootstrapのスタイルを設定します。

## Index

Scaffoldのビューにパンくずリストを追加して、さらにリンクをアイコン付きのボタンに変更します。
(`breadcrumb_tag`や`icon_tag`や`link_button_tag`は`TwitterBootstrap::Helper`のメソッドです。)

```erb:plugins/redmine_animal/app/views/animal_cats/index.html.erb
<%= breadcrumb_tag 'Cats' %>

<div class="page-header">
  <h1>Listing cats</h1>
</div>

<table class="table table-hover">
  <caption>
    <div class="pull-right">
      <%= link_button_tag('New', new_animal_cat_path, {action: 'primary', icon: 'plus', color: 'white'}) %>
    </div>
  </caption>
  <thead>
  <tr>
    <th><%=l :field_name %></th>
    <th><%=l :field_age %></th>
    <th><%=l :field_kitten %></th>
    <th></th>
  </tr>
  </thead>
  <tbody>
<% @cats.each do |cat| %>
  <tr>
    <td><%= link_to cat.name, cat %></td>
    <td><%= cat.age %></td>
    <td><%= icon_tag(icon: 'ok') if cat.is_kitten? %></td>
    <td>
      <%= link_button_tag('Edit', edit_animal_cat_path(cat), {action: 'success', icon: 'pencil', color: 'white'}) %>&nbsp;
      <%= link_button_tag('Destroy', cat, {action: 'danger', icon: 'trash', color: 'white'},
          {method: :delete, data: {confirm: 'Are you sure?'}}) %>
    </td>
  </tr>
<% end %>
  </tbody>
</table>
```

BootstrapはレスポンシブWebデザインに対応しています。

![animal_cat_index](https://dl.dropboxusercontent.com/u/14690051/images/notes/its/redmine/example/animal_cat_index.png)

## Show

`l`メソッドで国際化に対応します。日本語だと`<%=l :field_age %>`は`年齢`を表示します。

```erb:plugins/redmine_animal/app/views/animal_cats/show.html.erb
<%= breadcrumb_tag 'Show', [{caption: 'Cats', path: animal_cats_path}] %>

<div class="page-header">
  <h1>Cat</h1>
</div>

<% flash.each do |name, msg| %>
  <div class="alert alert-<%= name == :notice ? "success" : "error" %>">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    <%= msg %>
  </div>
<% end %>

<div class="row-fluid">
  <div class="row-fluid">
    <div class="pull-right">
      <%= link_button_tag('Edit', edit_animal_cat_path(@cat), {action: 'success', icon: 'pencil', color: 'white'}) %>
    </div>
  </div>

  <div class="row-fluid">
    <div class="span1 offset1"><%=l :field_name %>&nbsp;:</div>
    <div class="span1"><%= @cat.name %></div>
  </div>
  <div class="row-fluid">
    <div class="span1 offset1"><%=l :field_age %>&nbsp;:</div>
    <div class="span1"><%= @cat.age %></div>
  </div>

  &nbsp;

  <div class="row-fluid">
    <div class="span2 offset2">
      <%= link_button_tag('Destroy', @cat, {action: 'danger', icon: 'trash', color: 'white'},
          {method: :delete, data: {confirm: 'Are you sure?'}}) %>
    </div>    
  </div>
</div>
```

Showに`Edit`ボタンと`Destroy`ボタンがあればリストのボタンは冗長ですね。

![animal_cat_show](https://dl.dropboxusercontent.com/u/14690051/images/notes/its/redmine/example/animal_cat_show.png)


## New

追加と編集の[form](http://railsdoc.com/form)タグは部分テンプレートを利用します。
`<%= render 'form' %>`で`_form.html.erb`ファイルがインクルードされます。

```erb:plugins/redmine_animal/app/views/animal_cats/new.html.erb
<%= breadcrumb_tag 'New', [{caption: 'Cats', path: animal_cats_path}] %>

<div class="page-header">
  <h1>New cat</h1>
</div>

<%= render 'form' %>
```

[text_field](http://railsdoc.com/references/text_field)でテキストボックスが生成されます。
[number_field](http://railsdoc.com/references/number_field)で数値入力ボックスが生成されます。

![animal_cat_new](https://dl.dropboxusercontent.com/u/14690051/images/notes/its/redmine/example/animal_cat_new.png)


### form

モデルからのエラーは`alert-block `で表示しています。

```erb:plugins/redmine_animal/app/views/animal_cats/_form.html.erb
<%= form_for(@cat, html: {class: "form-horizontal"}) do |f| %>
  <% if @cat.errors.any? %>
    <div class="alert alert-error alert-block">
      <button type="button" class="close" data-dismiss="alert">&times;</button>
      <ul>
        <% @cat.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="control-group">
    <%= f.label :name, class: "control-label" %>
    <div class="controls">
      <%= f.text_field :name %>
    </div>
  </div>

  <div class="control-group">
    <%= f.label :age, class: "control-label" %>
    <div class="controls">
      <%= f.number_field :age %>
    </div>
  </div>

  <div class="control-group">
    <div class="controls">
      <%= f.submit class: "btn btn-primary" %>
    </div>
  </div>
<% end %>
```

![animal_cat_alert-block](https://dl.dropboxusercontent.com/u/14690051/images/notes/its/redmine/example/animal_cat_alert-block.png)


[submit](http://railsdoc.com/references/submit)タグの国際化対応は一括で設定できます。

```yml
ja:
  helpers:
    submit:
      create: Save
      update: Save
```


## Edit

NewとEditはほとんど同じレイアウトになります。

```erb:plugins/redmine_animal/app/views/animal_cats/edit.html.erb
<%= breadcrumb_tag 'Edit', [{caption: 'Cats', path: animal_cats_path}] %>

<div class="page-header">
  <h1>Editing cat</h1>
</div>

<div class="row-fluid">
  <div class="row-fluid">
    <div class="pull-right">
      <%= link_button_tag('Show', @cat, {action: 'info', icon: 'file', color: 'white'}) %>
    </div>
  </div>
</div>

<%= render 'form' %>
```

`Show`ボタンと`Edit`ボタンの位置を合わせると切り替えがスムーズですね。

![animal_cat_edit](https://dl.dropboxusercontent.com/u/14690051/images/notes/its/redmine/example/animal_cat_edit.png)

# Controller


最後はコントローラーです。リクエストとレスポンスの処理をします。
コントローラーのメソッドがアクションです。`params`にリクエスト情報がセットされています。


```ruby:plugins/redmine_animal/app/controllers/animal_cats_controller.rb
class AnimalCatsController < AnimalBaseController
  respond_to :html, :json

  def index
    respond_with(@cats = AnimalCat.all)
  end

  def show
    respond_with(@cat = AnimalCat.find(params[:id]))
  end

  def new
    respond_with(@cat = AnimalCat.new)
  end

  def edit
    @cat = AnimalCat.find(params[:id])
  end

  def create
    @cat = AnimalCat.new(params[:animal_cat])
    flash[:notice] = 'AnimalCat was successfully created.' if @cat.save
    respond_with(@cat, location: @cat)
  end

  def update
    @cat = AnimalCat.find(params[:id])
    flash[:notice] = 'AnimalCat was successfully updated.' if @cat.update_attributes(params[:animal_cat])
    respond_with(@cat, location: @cat)
  end

  def destroy
    @cat = AnimalCat.find(params[:id])
    @cat.destroy
    respond_with(@cat, location: animal_cats_url)
  end
end
```

コントローラーとモデルと合わせても50行程度でこの機能が実装できるのは***DRY***です。(ｷﾘｯ


# Tips

[before_filter](http://railsdoc.com/references/filter)に`require_admin`を設定すると管理者以外のアクセスを禁止することができます。

```ruby:plugins/redmine_animal/app/controllers/animal_base_controller.rb
class AnimalBaseController < ApplicationController
  unloadable
  layout 'animal'
  before_filter :require_admin
end
```