CanCanでモデルのないコントローラーで権限チェックを行う

ユーザー権限を見ながら、アクセスチェックをしようという目的で、DeviseとCanCanを使ってみています。まだよくわかってないんですが、これは便利そうです。
CanCanでは基本的にモデルのCRUDのURLをチェックする感じのチェックが行われます。

class HogesController < ApplicationController
  load_and_authorize_resource
  ...(略)
end

しかし、それに合わないようなコントローラーもあります。
各マスターデータ一覧を表示するようなやつです。

class MasterController < ApplicationController
  ...(略)
end

ここでload_and_authorize_resourceを指定すると、対象のMasterというモデルがないのでエラーで落ちます。
対象のモデルが無い場合はどうするのかを調べたところ、本家に書いてありました。
https://github.com/ryanb/cancan/wiki/Non-RESTful-Controllers

class MasterController < ApplicationController
  authorize_resource :class => false
  ...(略)
end

リソースがないんだからリソースclassをfalseに指定すりゃいいのね。なるほど。


validatesで:uniquenessを使う場合に注意するべきこと

Railsのmodelの検証で、

class Sample < ActiveRecord::Base
  belongs_to :hoge
  belongs_to :fuga
  attr_accessible :hoge, :fuga
  validates :hoge, :presence => true,
  validates :fuga, :presence => true, :uniqueness => {:scope => [:hoge]}
end

でRSpecのテストを通したところ、
undefined method `text?’ for nil:NilClass
というエラーが発生してしまった。

なんでやねん、と思っていたら、他のテストのところで、
PG::Error: ERROR: column samples.fuga does not exist
と言われていた。
どうも:uniquenessを指定する場合は、validatesで指定するのはカラム名でないといけないようだ。

validates の:fugaを:fuga_id,:uniquenessのところも:hoge_idに修正したところ、テストが通った。

class Sample < ActiveRecord::Base
  belongs_to :hoge
  belongs_to :fuga
  attr_accessible :hoge, :fuga
  validates :hoge, :presence => true,
  validates :fuga_id, :presence => true, :uniqueness => {:scope => [:hoge_id]}
end

割と悩んでしまった。


Rails:belongs_toの項目の検証方法

追記:inverse_ofを使いましょう。
http://319ring.net/blog/archives/2724

以下、原文。

Railsやってていつもbelongs_toの項目の検証をどうするのかで迷っていたのですが、自分はこれが最適解なのかなーと思ったのがあったので書いておきます。

belongs_toのデータを必須にしたいのですが、:presence => trueだと必須というだけでちゃんとリレーション先に存在するidじゃない場合があるので信頼性にかけるので、どうするべきか?と悩んでいました。:inclusionでやるのがいいのかな?と思っていたのですが、どうにも重たそうだし、ほかに方法があるんじゃないのかなーと。

Web+DB Pressの70号で紹介されているのですが、Rails3.2以降ならば、modelでpluckというメソッドが使えるようになっています。これは、指定したカラムの配列を返すメソッドです。モデルのオブジェクトが生成されることがないため、高速で動作します。

User.pluck(:id) #=> [1,2,3,4,5] みたいな感じ

これを使って、belongs_toで関連している項目の検証を行ってみました。

# coding: utf-8
class User < ActiveRecord::Base
  attr_accessble :name,
                 :role,
                 :role_id
  belongs_to :role, :readonly => true

  validates :role_id, :inclusion => {:in => Role.pluck(:id)}
  validates :name, :presence => true
end

これを検証してみるために、RSpecを書いてみます(FactoryGirlでfixture作ってます)。

# coding: utf-8
require 'spec_helper'

describe User do
  let(:role) {FactoryGirl.create(:role)}
  describe "正しいデータを入れた場合" do
    subject {FactoryGirl.build(:user, :role => role)}
    it("エラーがないこと") {should be_valid}
  end
end

一見、通りそうなのですが、エラーになりました…。
原因がわからなかったのでググってみたところ、:inclusion => {:in => Role.pluck(:id)}の、Role.pluck(:id)が評価されるタイミングがずれるため、エラーになっているようでした。ここを以下のようにprocで評価するように書き換えたら、ちゃんとテストが通りました。

# coding: utf-8
class User < ActiveRecord::Base
  attr_accessble :name,
                 :role,
                 :role_id
  belongs_to :role, :readonly => true

  validates :role_id, :inclusion => {:in => proc{Role.pluck(:id)}}
  validates :name, :presence => true
end

ここまで書いて、普通にRole.exists?(role_id)で評価すればよくね?と気づいた…。
なんでinclusionで比較しようとしていたんだ、俺は…。

最終的にこう直した。

# coding: utf-8
class User < ActiveRecord::Base
  attr_accessble :name,
                 :role,
                 :role_id
  belongs_to :role, :readonly => true

  validates :name, :presence => true
  validate :role_exists?

  private
  def role_exists?
    unless Role.exists?(role_id)
      errors.add(:role_id, I18n('activerecord.errors.messages.inclusion'))
    end
  end
end

たぶんこれが最適解なんだろうなー…。もっといい方法があるよ!という方がいらっしゃいましたら教えてください。


Rails:twitter-bootstrap-railsとsimple_formの組み合わせが素晴らしい

Railsをやっていると、twitter-bootstrapを使いたくなったときは、twitter-bootstrap-railsを使いますね。しかし、このbootstrap-railsで作ったフォームのエラー画面が残念すぎる…。ラベルが赤くなって、入力フォームも赤くなるだけ。formのclassにform-horizontalを付けてたらマジ残念。
なんとかする方法はないかなーと先日からいろいろと調べていたのですが、一番いいのを見つけました!それが、simple_formです!

simple_formはRailsでフォームを簡単に作るためのgemなんですが、私も使うのは初めてなので詳しくは知りません。知りませんが、twitter-bootstrap用の設定ができるので、これを使えばグンニョリするようなエラーフォームになりません。

  1. まず、Gemfileに以下を記述します。
    gem "simple_form", "~> 2.0.4"
    
  2. bundle installします。
  3. 以下のコマンドを実行します。
    rails g simple_form:install --bootstrap
    
  4. これで準備は整いました。
  5. Userの_form.html.erbを以下のようにしてみます。Userのvalidatesの定義でnameは必須にしてください。
    <%= simple_form_for @user, :html => {:class => 'form-horizontal'} do |f| %>
      <%= f.input :name, :input_html => {:class => 'input-medium'} %>
      <%= f.input :password, :input_html => {:class => 'input-medium'} %>
      <div class="form-actions">
        <%= f.button :submit, :class => 'btn-primary' %>
        <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
                    developers_path, :class => 'btn' %>
      </div>
    <% end %>
    

    labelタグの出力などをしなくても、f.input :name と書くだけで空気を読んでくれます。素晴らしい。

  6. エラーメッセージとボタンの日本語化をします。ja.ymlに以下のように書きます。
    ja:
      activerecord:
        errors:
          messages:
            blank: 必須入力です
            (…略…)
      helpers:
        titles:
          new: '%{model} 作成'
          edit: '%{model} 編集'
        links:
          cancel: キャンセル
        button:
            create: 作成
            update: 更新
        submit:
            create: 作成
            update: 更新
    
  7. これでエラーがある状態で作成ボタンを押してみてください。bootstrap的なエラー画面表示になってくれます。

simple_formはまだ使いだしたところですが、Rails的で記述量もすごく減るし、とてもありがたいですねぇ!


CodeIgniter: ユニットテストを書いてみる

CodeIgniterでユニットテストはどうするのか?読んだところ、ユニットテストクラスがあるようです。
http://codeigniter.jp/user_guide_ja/libraries/unit_testing.html

ただ、このテストクラスを動かすには、CI_Controller経由で動かすのが普通なのでしょうか。どうもブラウザからテストを起動するURLにアクセスしてテストするという感じっぽいですね。めんどいですね…。まぁほかの方法があるのかもしれませんが、とりあえずやってみます。

ひとまずモデルのテストを書いてみたかったので、それについて書きます。

  1. phpMyAdminでテスト用のデータベースを作成します。hoge_testとかでいいでしょう。
  2. その後、config/database.phpにテスト用データベース情報を定義します。$db[“default”]をコピーして、$db[“test”]を作成し、接続情報を変更しましょう。
  3. $active_groupを一旦、testにしましょう。マイグレーションを走らせるためです。マイグレーションを走らせたらdefaultに戻します。
  4. ここまでで、テスト用のデータベースの準備が整いました。
  5. 次に、テストケースを走らせるためのコントローラーを作成していきます。controller/testsフォルダを作成します。(実際は適当に。)
  6. testsフォルダの下に、testhoge.phpを作成し、Testhogeクラスを作ります。
    <?php
    class Testhoge extends CI_Controller{
    
      function __construct() {
        parent::__construct();
        $this->load->database('test');
        $this->db->trans_start(true);
        $this->load->model('Hoge');
        $this->load->library('unit_test');
        $this->unit->use_strict(true);
      }
    
      function index() {
        $this->test_hoge_insert();
        echo $this->unit->report();
      }
    
      function test_hoge_insert() {
        $id = $this->Hoge->insert("tests", "tests", 12345);
        $this->unit->run($id, 'is_numeric', "hogeを追加後にIDが返ってくること");
      }
    }
    
  7. では、コンストラクタから見ていきます。
      function __construct() {
        parent::__construct();
        $this->load->database('test');
        $this->db->trans_start(true);
        $this->load->model('Hoge');
        $this->load->library('unit_test');
        $this->unit->use_strict(true);
      }
    

    $this->load->database(‘test’)の引数’test’で、データベースに接続するグループを指定しています。これでテスト用のデータベースに接続しました。
    次に、$this->db->trans_start(true)を行っています。これは、トランザクションをテストモードで動かすための設定です。テストを行うたびにDBにデータが蓄積されてしまうので、不要なデータがたまらないように、どんな結果になろうともロールバックさせるために書いてます。
    CodeIgniterのユニットテストは、setUp, tearDownのような前処理・後処理を書くところがありません。やろうと思ったら全部自前で実装しないといけないのだと思います。1つずつテストをしたいときとか、ものすごく面倒ですねぇ…。まぁやる方法を思い浮かぶっちゃ浮かぶけど…。
    話がそれたので、続きを解説します。

  8. 次に、$this->load->model(‘Hoge’)でhogeテーブル用のモデルを読み込んでいます。そして、$this-load->library(‘unit_test’)でユニットテスト用のクラスを読み込んでいます。その後、型の検証を厳密するために、ユニットテストをstrictモードにしています。
  9. 次に、indexをみましょう。
      function index() {
        $this->test_hoge_insert();
        echo $this->unit->report();
      }
    

    $this->test_hoge_insert()で、ユニットテストを実行しています。test_hoge_insertメソッドは後で定義しています。そして、結果をecho $this->unit->report()で出力しています。やっていることはすごくシンプルで、あとはテストの数だけメソッドを追記するか、うまいことメタプログラミングでテストを実行させるか…っていうところです。

  10. 最後にテスト内容です。
      function test_hoge_insert() {
        $id = $this->Hoge->insert("tests", "tests", 12345);
        $this->unit->run($id, 'is_numeric', "hogeを追加後にIDが返ってくること");
      }
    

    $this->Hoge->insertは私が作ったHogeモデルでデータを投入しているだけです。戻り値はidで、失敗したらfalseが返ります。ちゃんと$idに整数が入っているかを確認します。
    $this->unit->run($id, ‘is_numeric’, ‘hogeを追加後にIDが返ってくること’)
    ですが、第1引数に検証対象、第2引数に検証ルール、第3引数に検証タイトルを記述します。

  11. この状態でhttp://localhost/tests/testhoge/にアクセスしたら、テストが実行されます。
  12. 結果は成功して、整数が戻っていました。そして、データベースにも挿入されたテストデータは残っていませんでした。

CodeIgniterのユニットテストは軽く始めてみるにはよさげな感じだけど、ドキュメントが、テストの始め方についての記述がちょっと不親切かなーという印象です。
setUpとtearDownについてはフックメソッドをうまく使えばできると思うので、改良の余地は十分にありますが、最初から準備しておいてほしいなぁという感じがしますね。うーん、でもデフォルトでコマンドラインでできるほうが助かるかな…。