ユカシカド エンジニアブログ

体の栄養状態を把握する検査サービス VitaNoteを開発するエンジニアのブログ

Rails製ECアプリのSpreeで販売終了期限を追加するエクステンションをつくる

西脇.rb & 東灘.rb 合同もくもく会 5th に参加してきました。 #nshgrb - 勉強課題: SpreeのExtensionをつくる。

で途中までしかできなくて、かつ、たくさんの懸念を残していた開発の続きをやりました。

内訳としては

  • メソッド名の再考
  • 既存メソッドとの衝突をなくす
  • テストを書く
  • Viewの実装

こんな感じです。

メソッド名(スコープ名)の再考

#buyable はやはり意味が大きすぎるので、#no_expires としました。

Spree::Product.no_expires

で、販売期限が過ぎていない商品の一覧を取得します。

既存メソッドとの衝突をなくす

Railsalias_method_chainっていうメソッドがあるよ」とマイ・ブラザーに教えてもらったので使ってみました。 ただ、今回オーバーライドしたいメソッドはインスタンスメソッドではなく、クラスメソッドになるので

Ruby - クラスメソッドを alias_method_chain - Qiita [キータ]

を参考にして、特異クラスを用意して定義しました。便利ですね!これを機にalias_method_chain、しっかり覚えておきたいです。

# spree_expiration_product/app/models/spree/product/scopes_decorator.rb
module Spree
  Product.class_eval do

    delegate_belongs_to :master, :expired_at
    attr_accessible :expired_at

    class << self
      def no_expires
        joins(:variants_including_master).where('spree_variants.expired_at >= ? or spree_variants.expired_at is null', Date.today)
      end

      def active_with_no_expires
        active_without_no_expires.no_expires
      end
      alias_method_chain :active, :no_expires
    end

  end
end

テストを書く

正直、テストもRSpecRailsもSpreeもあやふやな理解のままで書いてはみたものの、何をどこまでテストすべきなのか非常に悩みつつ書きました。

# spree_expiration_product/spec/models/spree/product/scope_decorator_spec.rb
require 'spec_helper'

describe Spree::Product do

  let(:product) {FactoryGirl.create(:product)}
  let(:variant) {product.master}
  let(:expired_date) {Date.today - 1.month}
  let(:not_expired_date) {Date.today + 1.month}

  describe '#no_expires' do

    let(:expired_products) {Spree::Product.no_expires}

    context "product is expired" do
      before do
        variant.expired_at = expired_date
        variant.save!
      end

      it "should be empty" do
        expect(expired_products).to be_empty
      end
    end

    context "product is not expired" do
      before do
        variant.expired_at = not_expired_date
        variant.save!
      end

      it "should be have 1 item" do
        expect(expired_products).to have(1).items
      end
    end

    context "product_variant.expired_at is nil" do
      before do
        variant.expired_at = nil
        variant.save!
      end
      it "should be have 1 item" do
        expect(expired_products).to have(1).items
      end
    end

  end

  describe '#active' do

    let(:active_products) {Spree::Product.active}

    context "product is expired" do
      before do
        variant.expired_at = expired_date
        variant.save!
      end

      it "should be empty" do
        expect(active_products).to be_empty
      end
    end

    context "product is not expired" do
      before do
        variant.expired_at = not_expired_date
        variant.save!
      end

      it "should be have 1 item" do
        expect(active_products).to have(1).items
      end
    end

    context "product_variant.expired_at is nil" do
      before do
        variant.expired_at = nil
        variant.save!
      end
      it "should be have 1 item" do
        expect(active_products).to have(1).items
      end
    end

  end

end

Viewの実装

SpreeではViewをオーバーライドする方法として、ターゲットとなるViewテンプレートをまるっと置き換える方法もあるのですが、アップデートについていけなくなったりするリスクを回避する方法として、Defaceというgemを使って、ピンポイントで必要な要素を差し込んだり、置き換えたりする方法が推奨されています。

# spree_expiration_product/app/overrides/add_expired_at_to_product_edit.rb
Deface::Override.new(:virtual_path => 'spree/admin/products/_form',
  :name => 'add_expired_at_to_product_edit',
  :insert_after => "code[erb-loud]:contains('text_field :available_on')",
  :text => "
    <%= f.field_container :expired_at do %>
      <%= f.label :expired_at, Spree.t(:expired_at) %>
      <%= f.error_message_on :expired_at %>
      <%= f.text_field :expired_at, :value => datepicker_field_value(@product.expired_at), :class => 'datepicker' %>
    <% end %>
  ")

簡単に解説すると、

  1. spree/admin/products/_form.erbの
  2. ‘text_field :available_on'を含むERBコードブロックの後に
  3. :textの値を挿入する

という内容です。挿入したいコンテンツが大きい場合などは:textの代わりに:partialを使って部分テンプレートを指定することもできます。

完成!

販売期限を設定したい商品の編集ページへいってみると、

http://localhost:3000/admin/products/product_name/edit

フィールドが追加されています。仮で過去の日付を入力して保存し

http://localhost:3000

で確認すると、表示されなくなります。

まとめ

実装にしてもテストにしても他のエクステンションのコードを見ながら、勉強していきたいと思います。

今後開発を検討しているエクステンションはもっと複雑で大きくなりそうなのですが、チュートリアル以外で実際に自分で調べながら動くものができたのは大きな収穫になりました。

ちなみに今回の成果物は引き続き、GitHubにあげています。

aq2bq/spree_expiration_product