==================================
前回はほぼScaffoldのみでRedmine Plugin(Redmine Animal)を作りました。
今回はRailsで開発の中心になるModel - View - Controller (MVC)を紹介します。
Scaffoldで出力されたファイルを修正してシンプルなデータのメンテナンス機能を実装します。
Model
まずはモデルです。データの操作とビジネスロジックを記述します。
データ操作はActiveRecord::Base
を継承しているのでO/Rマッピングが利用できます。
また、モデルレベルでデータを検証するにはvalidatesを利用します。
AnimalCat
クラスはname
を必須入力にます。さらに値の一意を検証します。
age
は値が0
から99
の範囲に含まれているか検証します。
(Railsはvalidates presence of name.
のようにメソッドで英文になります。)
インスタンスメソッドのis_kitten?
は1歳未満を子猫と判断します。
(Rubyはメソッドで最後に評価した式の結果を返します。)
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)のコンソールで起動します。
モデルクラスからデータを操作したりビジネスロジックの実行などをします。
$ 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の他にhamlやslimなどがあります。
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
ファイルに作ります。
<!DOCTYPE html>
<html>
<head>
<title>Animal</title>
</head>
<body>
<%= yield %>
</body>
</html>
他のリソースにもレイアウトを利用するケースがあるのでApplicationController
を
継承したAnimalBaseController
をanimal_base_controller.rb
ファイルに作ります。
class AnimalBaseController < ApplicationController
unloadable
layout 'animal'
end
AnimalCatsController
の継承をApplicationController
からAnimalBaseController
に変更します。
class AnimalCatsController < AnimalBaseController
...
end
これでスタイルのない状態になりました。スタイルはシンプルで機能的なデザインができるBootstrapを使います。
BootstrapはRedmine Twitter Bootstrap Plugin
で簡単に使えるようになります。
$ git clone git://github.com/ogom/redmine_twitter_bootstrap ./plugins/redmine_twitter_bootstrap
先ほどのanimal.html.erb
ファイルにBootstrapのスタイルシートとジャバスクリプトを追加します。
<!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
にインクルードします。
module AnimalBaseHelper
include RedmineTwitterBootstrap::Helper
end
ナビバーが表示されるようになりました。
他のファイルにもBootstrapのスタイルを設定します。
Index
Scaffoldのビューにパンくずリストを追加して、さらにリンクをアイコン付きのボタンに変更します。
(breadcrumb_tag
やicon_tag
やlink_button_tag
はTwitterBootstrap::Helper
のメソッドです。)
<%= 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'}) %>
<%= 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デザインに対応しています。
Show
l
メソッドで国際化に対応します。日本語だと<%=l :field_age %>
は年齢
を表示します。
<%= 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">×</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 %> :</div>
<div class="span1"><%= @cat.name %></div>
</div>
<div class="row-fluid">
<div class="span1 offset1"><%=l :field_age %> :</div>
<div class="span1"><%= @cat.age %></div>
</div>
<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
ボタンがあればリストのボタンは冗長ですね。
New
追加と編集のformタグは部分テンプレートを利用します。
<%= render 'form' %>
で_form.html.erb
ファイルがインクルードされます。
<%= breadcrumb_tag 'New', [{caption: 'Cats', path: animal_cats_path}] %>
<div class="page-header">
<h1>New cat</h1>
</div>
<%= render 'form' %>
text_fieldでテキストボックスが生成されます。
number_fieldで数値入力ボックスが生成されます。
form
モデルからのエラーはalert-block
で表示しています。
<%= 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">×</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 %>
submitタグの国際化対応は一括で設定できます。
ja:
helpers:
submit:
create: Save
update: Save
Edit
NewとEditはほとんど同じレイアウトになります。
<%= 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
ボタンの位置を合わせると切り替えがスムーズですね。
Controller
最後はコントローラーです。リクエストとレスポンスの処理をします。
コントローラーのメソッドがアクションです。params
にリクエスト情報がセットされています。
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にrequire_admin
を設定すると管理者以外のアクセスを禁止することができます。
class AnimalBaseController < ApplicationController
unloadable
layout 'animal'
before_filter :require_admin
end