LoginSignup
8
9

More than 5 years have passed since last update.

MVC of Redmine Plugin in Bootstrap

Last updated at Posted at 2013-06-03

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

前回はほぼ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はメソッドで最後に評価した式の結果を返します。)

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)のコンソールで起動します。
モデルクラスからデータを操作したりビジネスロジックの実行などをします。

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

View

次はビューです。レスポンスのテンプレートを記述します。
テンプレートエンジンはerbの他にhamlslimなどがあります。

Layout

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

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

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

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

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

<%= yield %>

</body>
</html>

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

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

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

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

これでスタイルのない状態になりました。スタイルはシンプルで機能的なデザインができるBootstrapを使います。

animal_cat_layout_simple

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

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

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

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にインクルードします。

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

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

animal_cat_layout_tb

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

Index

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

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

Show

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

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

New

追加と編集のformタグは部分テンプレートを利用します。
<%= render 'form' %>_form.html.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でテキストボックスが生成されます。
number_fieldで数値入力ボックスが生成されます。

animal_cat_new

form

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

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

submitタグの国際化対応は一括で設定できます。

ja:
  helpers:
    submit:
      create: Save
      update: Save

Edit

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

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

Controller

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

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_filterrequire_adminを設定すると管理者以外のアクセスを禁止することができます。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9