News

Welcome to End Point’s blog

Ongoing observations by End Point people

Spree and Software Development: Git and Ruby techniques

Having tackled a few interesting Spree projects lately, I thought I'd share some software evelopment tips I've picked up along the way.

Gem or Source?

The first decision you may need to make is whether to run Spree from a gem or source. Directions for both are included at the Spree Quickstart Guide, but the guide doesn't touch on motivation from running from a gem versus source. The Spree documentation does address the question, but I wanted to comment based on recent experience. I've preferred to build an application running from the gem for most client projects. The only times I've decided to work against Spree source code was when the Spree edge code had a major change that wasn't available in a released gem, or if I wanted to troubleshoot the internals of Spree, such as the extension loader or localization functionality.

If you follow good code organization practices and develop modular and abstracted functionality, it should be quite easy to switch back and forth between gem and source. However, switching back and forth between Spree gem and source may not be cleanly managed from a version control perspective.

git rebase

Git rebase is lovely. Ethan describes some examples of using git rebase here. When working with several other developers and even when I'm the sole developer, I've included rebasing in my pull and push workflow.

.gitmodules

Git submodules are lovely, also. An overview on git submodules with contributions from Brian Miller and David Christensen can be read here. Below is an example of a .gitmodules from a recent project that includes several extensions written by folks in the Spree community:

[submodule "vendor/extensions/faq"]
        path = vendor/extensions/faq
        url = git://github.com/joshnuss/spree-faq.git
[submodule "vendor/extensions/multi_domain"]
        path = vendor/extensions/multi_domain
        url = git://github.com/railsdog/spree-multi-domain.git
[submodule "vendor/extensions/paypal_express"]
        path = vendor/extensions/paypal_express
        url = git://github.com/railsdog/spree-paypal-express.git

.gitignore

This should apply to software development for other applications as well, but it's important to setup .gitignore correctly at the beginning of the project. I typically ignore database, log, and tmp files. Occasionally, I ignore some public asset files (stylesheets, javascripts, images) if they are copied over from an extension upon server restart, which is standard in Spree.

Overriding modules, controllers, and views

Now, the good stuff! So, let's assume the Spree core is missing functionality that you need. Options for expanding from the Spree core include overriding or extending existing models, controllers, or views, or writing and including new models, controllers, or views. Spree's Extension Tutorial covers adding new controllers, models, and views, so I'll discuss extending and overriding existing models, views and controllers below.

Extend an Existing Controller

To extend an existing controller, I've typically included a module with the extended behavior in the *_extension.rb file. For all examples, let's assume that my extension is named "Site", another standard in Spree. The code below shows the module include in site_extension.rb:

...
def activate
  ProductsController.send(:include, Spree::Site::ProductsController)
end
...

My ProductsController module, inside the Spree::Site namespace, includes the following to define a before filter in the Spree core products controller:

module Spree::Site::ProductsController
  def self.included(controller)
    controller.class_eval do
      controller.append_before_filter :do_stuff
    end
  end

  def do_stuff
    #doing stuff
  end
end

Override an Existing Controller Method

Next, to override a method in an existing controller, I've started the same way as before, by including a module in site_extension.rb:

...
def activate
  CheckoutsController.send(:include, Spree::Site::CheckoutsController)
end
...

The Spree::Site::CheckoutsController module will contain:

module Spree::Site::CheckoutsController
  def self.included(target)
    target.class_eval do
      alias :spree_rate_hash :rate_hash
      def rate_hash; site_rate_hash; end
    end
  end

  def site_rate_hash
    # compute new rate_hash
  end
end

In this example, the core rate_hash method is aliased for later use. And the rate_hash method is redefined inside the class_eval block. This example demonstrates how to override the core shipping rate computation during checkout.

Extend an Existing Model

Next, I'll provide an example of extending an existing Spree model. site_extension.rb will include the following:

...
def activate
  Product.send(:include, Spree::Site::Product)
end
...

And Spree::Site::Product module contains:

module Spree::Site::Product
  def new_product_method
    # new product instance method
  end
end

In the situation where you may want to create a class object method rather than an instance object method, you may include the following in Spree::Site::Product:

module Spree::Site::Product
  def self.included(target)
    def target.do_something_special
      'Something Special!'
    end
  end
end

The above example adds a method to the Product class object to be called from a view, for example <%= Product.do_something_special %> will return 'Something Special!'.

Override an Existing Model Method

To override a method from an existing model, I start with a module include in site_extension.rb:

...
def activate
  Product.send(:include, Spree::Site::Product)
end
...

And Spree::Site::Product contains the following:

module Spree::Site::Product
  def self.included(model)
    model.class_eval do
      alias :spree_master_price :master_price
      def master_price; site_master_price; end
    end
  end
  def site_master_price
    '1 billion dollars'
  end
end 

And from the view, the two methods can be called within the following block:

<% @products.each do |product| -%>
<%= product.master_price %> vs <%= product.spree_master_price.to_s %>
<% end -%>

Extend an Existing View

I previously discussed the introduction of hooks in depth here and here. To extend an existing view that has a hook wrapped around the content you intend to modify, you may add something similar to the following to *_hooks.rb, where * is the extension name:

insert_after :homepage_products, 'shared/promo'

The above code inserts the 'shared/promo' view to be rendered above the homepage_products hook in the Spree gem or Spree source ~/app/views/products.index.html.erb view. Other hook actions include insert_before, replace, or remove.

Override an Existing View

Before the introduction of hooks, the standard method of overriding or extending core views was to copy the core view into your extension view directory, and apply changes. In some cases, hooks are not always in the desired location. To override the footer view since there is no footer hook, I copy the Spree gem footer view to the extension view directory. The diff below compares the Spree gem view and my extension footer view:

- <div id="footer">
-  <div class="left">
-    <p>
-      <%= t("powered_by") %> <a href="http://spreecommerce.com/">Spree</a>
-    </p>
-  </div>
-  <div class="right">
-    <%= render 'shared/language_bar' if Spree::Config[:allow_locale_switching] %>
-  </div>
- </div>
<%= render 'shared/google_analytics' %>
+<p><a href="http://www.endpoint.com/">End Point</a></p>

Sample data

A final tip that I've found helpful when developing with Spree is to create sample data files in the extension db directory to maintain data consistency between developers. In a recent project, I've created the following stores.yml data to initiate several stores for the multi domain extension:

store_1:
  name: Store1
  code: store1
  domains: store1.mysite.com
  default: false
store_2:
  name: Store2
  code: store2
  domains: store2.mysite.com
  default: true
store_3:
  name: Store3
  code: store3
  domains: store3.mysite.com
  default: false

Many of these tips apply to general to software development. The tips specific to development in Spree (and possibly other Rails platforms) include the sample data syntax and the described Ruby techniques to extend and override class and model functionality.

Learn more about End Point's Ruby on Rails Development or Ruby on Rails Ecommerce Services.

2 comments:

Stanley said...

Hi,

I followed the instruction in the code but met an problem.

When I use the code in that way and startup the server, it told me that
:in `load_missing_constant': uninitialized constant Spree::ComplexCommission (NameError)

The ComplexCommission is the extension I created. I am wondering if you could help me why it looks that the Spree:ComplexCommission:Product module I added in app/models in the extension didn't work.

Thanks in advance

Steph Powell said...

Hi Stanley,

I'd probably recommend you email the spree-user Google group with code samples and the errors you are seeing. I frequently respond to issues there and your problems can be addressed by a larger audience. Please include snippets of how you are including the module and the module itself.

~Steph