action_controllerの拡張gemをRSpecでTDD ٩( ‘ω’ )و

action_controllerの拡張gemを作成するときにrspecでテストする

ユーザ認証のサンプルgemをTDDで作っていきましょう

まずは、gemのひな形を作る

$ bundle gem user_auth

bundlerが1.9系なら以下のファイルが作成されてるはず

├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│   ├── console
│   └── setup
├── lib
│   ├── user_auth
│   │   └── version.rb
│   └── user_auth.rb
├── spec
│   ├── spec_helper.rb
│   └── user_auth_spec.rb
└── user_auth.gemspec

さて、ここから本題です

ゴールはこんな感じで、認証に失敗したら例外みたいなー

class ApplicationControlller < ActionController::Base
  before_action :authenticate_user!
end

TDDなのでテストから書いてくが、その前に開発時に必要なgemをインストール

# Gemfile
source 'https://rubygems.org'

gemspec

# この3つを追加
gem 'rails'
gem 'rspec-rails'
gem 'sqlite3'

ざっとテスト書いていきまーす

# spec/user_authenticate_spec.rb
require 'spec_helper'

RSpec.describe UserAuth::UserAuthenticate do
  describe ApplicationController, type: :controller do
    controller do
      def index
        render text: 'success!'
      end
    end

    context 'Authentication failure' do
      example do
        expect { get :index }.to raise_error
      end
    end
    
    context 'Authentication success' do
      let(:user) { User.create(email: 'user@example.com') }

      example do
        session[:user_id] = user.id
        get :index

        expect(response.body).to eq 'success!'
      end
    end
  end
end

テスト実行すると落ちます! uninitialized constant UserAuth::UserAuthenticate (NameError)

UserAuthenticateがないよーって言ってるので作りましょう

# lib/user_auth/user_authenticate.rb
module UserAuth
  module UserAuthenticate
  end
end

# spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'user_auth'
# 追加
require 'user_auth/user_authenticate'

テストを実行! uninitialized constant ApplicationController (NameError)

ApplicationControllerがないって言ってるので、ダミーのコントローラーを作ります

# spec/support/action_controller.rb
require 'action_controller/base'

class ApplicationController < ActionController::Base
end

# spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'user_auth'
# 追加
require 'support/action_controller'

テストを実行! 'method_missing': undefined method 'controller'

RSpecのcontrollerメソッドがないっていってる。

これはrspec-railsメソッドなので、spec_helperに追加

# spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'user_auth'
require 'support/action_controller'
# 追加
require 'rspec/rails'

テストを実行! undefined method 'application' for Rails:Module

applicationがないって言ってるのでダミーのRails::Applicationを作ります

# spec/support/application.rb
require 'action_dispatch'

module Rails
  class App
    def env_config; {} end
    def routes
      return @routes if defined? @routes
      @routes = ActionDispatch::Routing::RouteSet.new
      @routes.draw do
        resources :posts
      end
      @routes
    end
  end

  def self.application
    @app ||= App.new
  end
end

# spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'user_auth'
# action controllerより前に追加
require 'support/application'
require 'support/action_controller'
require 'rspec/rails'

テストを実行! uninitialized constant User

Userがないって言ってるのでダミーのUserモデルを作ります

# spec/support/active_record.rb
require 'active_record'

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')

class User < ActiveRecord::Base; end

class CreateAllTables < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :email
    end
  end
end

# spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'user_auth'
require 'user_auth/user_authenticate'
require 'support/application'
require 'support/action_controller'
require 'rspec/rails'
# 追加
require 'support/active_record'

RSpec.configure do |config|
  config.before :all do
    CreateAllTables.up unless ActiveRecord::Base.connection.table_exists? 'users'
  end

  config.before :each do
    User.delete_all
  end
end

テストを実行! For instance, 'include Rails.application.routes.url_helpers'.

そのまま、ダミーのApplicationControllerにincludeしてあげましょう

# spec/support/action_controller.rb
require 'action_controller'
class ApplicationController < ActionController::Base
  include Rails.application.routes.url_helpers
end

テストを実行! expected Exception but nothing was raised

キタ——(゚∀゚)——!!

ダミーのApplicationControllerにauthenticate_user!メソッドを追加しましょう

# spec/support/action_controller.rb
require 'action_controller'
class ApplicationController < ActionController::Base
  include Rails.application.routes.url_helpers

  before_action :authenticate_user!
end

テストを実行! undefined method 'authenticate_user!'

authenticate_user!メソッドを実装します

# lib/user_auth/user_authenticate.rb
module UserAuth
  module UserAuthenticate
    def authenticate_user!
    end
  end
end

# lib/railtie.rb
require 'rails/railtie'
require 'active_support'

module UserAuth
  class Railtie < Rails::Railtie
    ActiveSupport.on_load :action_controller do
      require 'user_auth/user_authenticate'
      send :include, UserAuth::UserAuthenticate
    end
  end
end

# lib/user_auth.rb
require "user_auth/version"
# 追加
require 'user_auth/railtie'
...

テストを実行! expected Exception but nothing was raised

authenticate_user!メソッドでユーザを取得する

# lib/user_auth/user_authenticate.rb
module UserAuth
  module UserAuthenticate
    def authenticate_user!
      User.find(session[:user_id])
    end
  end
end

テストを実行!

f:id:murajun1978:20150415030247p:plain

おーるぐりーんヾ( ̄∇ ̄=ノ

どうですか?

TDDなら次に何をするかが明確になりますよね?

テストを書いて、エラーがでたら原因を調べて解消していく

テストが失敗(エラーじゃないよ)したらロジックを書く!

そしてグリーンになればオッケーって感じです

サンプルコードは雑ですがね...

Happy Hacking٩( ‘ω’ )و

Rails - ActiveDecoratorのコード読んでみたヘ(^o^)ノ

いつもお世話になります。ActiveDecoratorさま。

@izumin5210さんの「ActiveDecorator読んでみたら超勉強になった」を読んで、そういやコードあんま見てなかったので読んでみた

なるほど編

コードよむ編

# lib/active_decorator/monkey/abstract_controller/rendering.rb
module AbstractController
  module Rendering
    # to_aメソッドのエイリアス↓
    def view_assigns_with_decorator 
      # 既存のto_aメソッドをコール
      hash = view_assigns_without_decorator

      # メソッドを拡張
      hash.values.each do |v|
        ActiveDecorator::Decorator.instance.decorate v
      end
      hash
    end
    
    # :view_assigns => :view_assigns_with_decorator
    # :view_assigns_without_decorator => :view_assigns
    alias_method_chain :view_assigns, :decorator
  end
end

view_assignsは、Controllerで取得したインスタンスなんかが格納されたHashを返しますよー

こんな感じ↓

f:id:murajun1978:20150114225602p:plain

postsはActiveRecord::Relationです(Post.allしたやつ)

testはインスタンス変数に文字列をいれてみた

このHashをActiveDecorator::Decorator

# lib/active_decorator/decorator.rb
module ActiveDecorator
  class Decorator
    include Singleton

    def initialize
      @@decorators = {}
    end

    def decorate(obj)
      [...]
    end
    [...]
  end
end

singletonをMix-inしてるので、ActiveDecorator::Decorator.instance.decorateで呼び出してますよ

decorateメソッド
if obj.is_a?(Array)
  # *2でArrayが引数となるのでここ
  # Arrayの要素(ModelClass)を引数として自身を呼び出してる *3
  obj.each do |r|
    decorate r
  end
elsif defined?(ActiveRecord) && obj.is_a?(ActiveRecord::Relation) && !obj.respond_to?(:to_a_with_decorator)
  # ActiveRecord::Relationならクラスを再オープンしてメソッド定義してる *1
  class << obj
    def to_a_with_decorator
      to_a_without_decorator.tap do |arr|
        # Arrayを引数として自身を呼び出してる *2
        ActiveDecorator::Decorator.instance.decorate arr
      end
    end
    alias_method_chain :to_a, :decorator
  end
else
  # *3で引数がmodelクラスとなるのでここ(ActiveRecord::Relation、Array以外もここ)
  # decoratorのクラス取得してる
  d = decorator_for obj.class
  return obj unless d
  # PostDecoratorモジュールに属してなければextendしてる
  obj.extend d unless obj.is_a? d  
end
decorator_forメソッド
private
def decorator_for(model_class)
  return @@decorators[model_class] if @@decorators.has_key? model_class

  decorator_name = "#{model_class.name}Decorator"
  d = decorator_name.constantize
  unless Class === d
    d.send :include, ActiveDecorator::Helpers
    @@decorators[model_class] = d
  else
    @@decorators[model_class] = nil
  end
rescue NameError
  @@decorators[model_class] = nil
end

ViewContextをごっそり

# lib/active_decorator/view_context.rb
module Filter
  extend ActiveSupport::Concern

  included do
    before_filter do |controller|
      ActiveDecorator::ViewContext.current = controller.view_context
    end
  end
end

helperの拡張

# lib/active_decorator/helpers.rb
module Helpers
  def method_missing(method, *args, &block)
    super
  #TODO need to make sure who raised the error?
  rescue NoMethodError, NameError => original_error
    begin
      # helperでmethod_missingならActiveDecorator::ViewContextから(なければoriginal_errorの例外発生
      ActiveDecorator::ViewContext.current.send method, *args, &block
    rescue NoMethodError, NameError
      raise original_error
    end
  end
end

ViewContextをごっそりとってるから、flashとかもdecoratorで使えるよー

ActiveDecorator::ViewContext.current.flash 
=> #<ActionDispatch::Flash::FlashHash:0x007fda40e27d78 @discard=#<Set: {}>, @flashes={"error"=>"test error"}, @now=nil>
ActiveDecorator::ViewContext.current.flash[:error]
=> "test error"

# app/decorators/post_decorator.rb
module PostDecorator
  def error_flash
    flash[:error]
  end
end

ActiveDecoratorだと読みやすくて勉強にも良いかと思います♪

Happy Hacking٩( ‘ω’ )و

d(゚Д゚ )☆スペシャルサンクス☆( ゚Д゚)b

Railsでpostした時間を○○minutes agoみたいに表示するヘ(^o^)ノ

今日、Twitter@netwillnetさんと@chiastoliteさんのツイートをみてやってみた

Railsではtime_ago_in_wordsヘルパーが用意されています

でも、この子はリアルタイムで更新されない(当たり前

クライアントサイドで、リアルタイムに経過時間をとってきたいですよね?

jQueryプラグインtimeagoってのがあります

これを使ってみましょう

jsファイルを... ダウンロードせずにgemを探します

rails-timeago - GitHubを使ってみましょう

使い方はとっても簡単♪

# Gemfile
gem 'rails-timeago', '~> 2.0'

# app/assets/javascripts/application.js
//= require rails-timeago

# app/views/posts/index.html.haml
- @posts.each do |post|
  %tr
    %td= timeago_tag post.created_at, nojs: true, limit: 10.days.ago

しばらく何もせずに待っていると...

クライアントサイドで経過時間が更新されていると思いますヘ(^o^)ノ

これでタイムラインの経過時間がリアルタイムに更新されますねー

Happy Hacking٩( ‘ω’ )و

Gretelで簡単パンくずリストヘ(^o^)ノ

Railsパンくずリストを表示するのにGretelをつかってみた

基本的な使い方ー

# config/breadcrumbs.rb
crumb :root do
  link 'Home', root_path
end

crumb :posts do
  link 'Posts', posts_path
end

crumb :post do |post|
  link post.title, post
  parent :posts
end

# views/layouts/application.html.erb
<%= breadcrumbs separator: " &rsaquo; " %>

# views/posts/index.html.erb
<% breadcrumb :posts %>

これでHome > Posts > ほげほげみたく表示される

うーん、素敵 ☆ミ

同一ページでパンくず使いたいとき

# config/breadcrumbs.rb
[...]
crumb :comments do |post|
  link 'Comments', comments_path(post)
end

# views/posts/show.html.erb
<% breadcrumb @post %>

<% with_breadcrumb :comments @post do %>
  <%= breadcrumbs separator: " &rsaquo; " %>
<% end %>

オプションも豊富! Gretel - Options

bootstrapやfoundation5もstyleオプションで指定すれば使えます。

Controllerと分けて管理できるのでさらに良いのですヘ(^o^)ノ

Happy Hacking٩( ‘ω’ )و

RansackでDateTime型の検索してみるヘ(^o^)ノ

Ransackはとっても便利ですよねー

さらっとしか使えてないので、ちょっと調べてみました

eqは完全一致、contは中間一致

などなどとっても簡単に検索機能を実装できます

そこでDateTime型のフィールドを検索したい場合、時分秒までは検索しないですよね?

やりたいことはDateTime型のフィールドをDate型で検索したいので

# in the model :
ransacker :created_at do
  Arel::Nodes::SqlLiteral.new "date(items.created_at)"
end

SQLを確認してみる

SELECT "projects".* FROM "projects"  WHERE ((date(projects.created_at) >= '2014-11-03' AND date(projects.created_at) <= '2014-11-03'))

これでyyyy-mm-ddの日付型で検索できますねー

文字列 yyyy/mm/ddで検索したい場合はこう

# in the model
ransacker :created_at do
  Arel::Nodes::SqlLiteral.new "strftime('%Y/%m/%d', projects.created_at)"
end

SQLを確認してみる

SELECT "projects".* FROM "projects"  WHERE ((strftime('%Y/%m/%d', projects.created_at) >= '2014/11/03' AND strftime('%Y/%m/%d', projects.created_at) <= '2014/11/03'))

できたーヘ(^o^)ノ

入力された文字列を日付型として扱いたい場合は、以下の設定ファイルを追加してあげよう

Ransack.configure do |config|
  config.add_predicate 'date_lteq',
  arel_predicate: 'lteq',
  formatter: proc { |v| v.to_date },
  validator: proc { |v| v.present? },
  type: :string
end

これでcreated_at_date_lteqとすれば、yyyy/mm/ddと入力されても日付型に変化してくれます

注意) このコードはSQLite用ですm(__)m

PostgreSQLMySQLは、

Arel::Nodes::SqlLiteral.new => Arel.sqlでいけると思います

もちろんDate関数も変わってきますのでご注意を

と色々やってきたとこで、このQiita記事をみると... 検索用のgem「ransack」を使ってみる

日付型のFrom - Toはこれで良いみたいですなー

Ransack.configure do |config|
  config.add_predicate 'date_lteq',
  arel_predicate: 'lteq',
  formatter: proc { |v| v.end_of_day }, # ここでend_of_dayメソッドコールしてる
  validator: proc { |v| v.present? }
end

なるほど...そやね( ̄▽ ̄;)

すごい遠回り感がハンパないけど、いいやw

Happy Hacking٩( ‘ω’ )و

参考サイト

検索用のgem「ransack」を使ってみる

Using Ransackers