spree_variant_options 0.1.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ .DS_Store
4
+ Gemfile.lock
5
+ pkg/*
6
+ public/tmp.html
7
+ lib/dummy_hooks/after_migrate.rb
8
+ test/dummy
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2011 Spencer Steffen and Citrus, released under the New BSD License All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification,
4
+ are permitted provided that the following conditions are met:
5
+
6
+ * Redistributions of source code must retain the above copyright notice,
7
+ this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright notice,
9
+ this list of conditions and the following disclaimer in the documentation
10
+ and/or other materials provided with the distribution.
11
+ * Neither the name of the Rails Dog LLC nor the names of its
12
+ contributors may be used to endorse or promote products derived from this
13
+ software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
19
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,129 @@
1
+ Spree Variant Options
2
+ =====================
3
+
4
+ Spree Variant Options is a very simple spree extension that groups your variants by option types and values. To get a better idea let's let a few images do the explaining.
5
+
6
+
7
+ #### When no selection has been made:
8
+ ![Spree Variant Options - No selection](http://spree-docs.s3.amazonaws.com/spree_variant_options/1.jpg)
9
+
10
+ #### After "Medium" is selected, "Medium Blue" is out of stock:
11
+
12
+ ![Spree Variant Options - Option Type/Value selected](http://spree-docs.s3.amazonaws.com/spree_variant_options/2.jpg)
13
+
14
+ #### And after "Green" is selected:
15
+ ![Spree Variant Options - Variant Selcted](http://spree-docs.s3.amazonaws.com/spree_variant_options/3.jpg)
16
+
17
+ To see it in action, follow the steps for "Demo" below.
18
+
19
+
20
+ Installation
21
+ ------------
22
+
23
+ If you don't already have an existing Spree site, [click here](https://gist.github.com/946719) then come back later... You can also read the Spree docs [here](http://spreecommerce.com/documentation/getting_started.html)...
24
+
25
+ Spree Variant Options hasn't been released to rubygems so you'll have to install it from the source. Just add the following to your Gemfile:
26
+
27
+ gem 'spree_variant_options', :git => 'git://github.com/citrus/spree_variant_options.git'
28
+
29
+ Now, bundle up with:
30
+
31
+ bundle
32
+
33
+ Spree Variant Options doesn't require any rake tasks or generators, but you'll need include `app/views/products/_variant_options.html.erb` in your product show view.
34
+
35
+ If you don't have a custom version of `_cart_form.html.erb` in your application, then don't worry about a thing, spree_variant_options will include the partial for you. Otherwise, just replace the entire `<% if @product.has_variants? %>` block with:
36
+
37
+ <%= render 'variant_options' %>
38
+
39
+
40
+ Versions
41
+ --------
42
+
43
+ Spree Variant Options works on Spree 0.30.1 and above... Please let me know if you run into any issues.
44
+
45
+
46
+ Testing
47
+ -------
48
+
49
+ Clone this repo to where you develop, bundle up, then run `dummier' to get the show started:
50
+
51
+ git clone git://github.com/citrus/spree_variant_options.git
52
+ cd spree_variant_options
53
+ bundle install
54
+ bundle exec dummier
55
+
56
+
57
+ This will generate a fresh rails app in test/dummy, install spree & spree_variant_options, then migrate the test database. Sweet.
58
+
59
+
60
+ ### Spork + Cucumber
61
+
62
+ To run the cucumber features, boot spork like this:
63
+
64
+ bundle exec spork
65
+
66
+ Then, in another window, run:
67
+
68
+ cucumber --drb
69
+
70
+
71
+ ### Spork + Test::Unit
72
+
73
+ If you want to run shoulda tests, start spork with:
74
+
75
+ bundle exec spork TestUnit
76
+ #or
77
+ bundle exec spork t
78
+
79
+ In another window, run all tests:
80
+
81
+ testdrb test/**/*_test.rb
82
+
83
+ Or just a specific test:
84
+
85
+ testdrb test/unit/supplier_test.rb
86
+
87
+
88
+ ### No Spork
89
+
90
+ If you don't want to spork, just use rake:
91
+
92
+ # cucumber/capybara
93
+ rake cucumber
94
+
95
+ # test/unit
96
+ rake test
97
+
98
+ # both
99
+ rake
100
+
101
+ POW!
102
+
103
+
104
+ Demo
105
+ ----
106
+
107
+ You can easily use the test/dummy app as a demo of spree_variant_options. Just `cd` to where you develop and run:
108
+
109
+ git clone git://github.com/citrus/spree_variant_options.git
110
+ cd spree_variant_options
111
+ mv lib/dummy_hooks/after_migrate.rb.sample lib/dummy_hooks/after_migrate.rb
112
+ bundle install
113
+ bundle exec dummier
114
+ cd test/dummy
115
+ rails s
116
+
117
+
118
+ Contributors
119
+ ------------
120
+
121
+ So far it's just me; Spencer Steffen.
122
+
123
+ If you'd like to help out feel free to fork and send me pull requests!
124
+
125
+
126
+ License
127
+ -------
128
+
129
+ Copyright (c) 2011 Spencer Steffen and Citrus, released under the New BSD License All rights reserved.
@@ -0,0 +1,26 @@
1
+ # encoding: UTF-8
2
+ require 'rubygems'
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ #require 'rake'
10
+ require 'rake/testtask'
11
+
12
+ Bundler::GemHelper.install_tasks
13
+
14
+ Rake::TestTask.new(:test) do |t|
15
+ t.libs << 'lib'
16
+ t.libs << 'test'
17
+ t.pattern = 'test/**/*_test.rb'
18
+ t.verbose = false
19
+ end
20
+
21
+ require 'cucumber/rake/task'
22
+ Cucumber::Rake::Task.new do |t|
23
+ t.cucumber_opts = %w{--format pretty}
24
+ end
25
+
26
+ task :default => [ :test, :cucumber ]
@@ -0,0 +1,4 @@
1
+ "0.60.x" => { :branch => "master" }
2
+ "0.50.x" => { :branch => "master" }
3
+ "0.40.x" => { :branch => "master" }
4
+ "0.30.x" => { :branch => "master" }
@@ -0,0 +1,27 @@
1
+ Product.class_eval do
2
+
3
+ include ActionView::Helpers::NumberHelper
4
+
5
+ def option_values
6
+ option_types.map{|i| i.option_values }.flatten.uniq
7
+ end
8
+
9
+ def grouped_option_values
10
+ option_values.group_by(&:option_type)
11
+ end
12
+
13
+ def variant_options_hash
14
+ return @variant_options_hash if @variant_options_hash
15
+ @variant_options_hash = Hash[grouped_option_values.map{ |type, values|
16
+ [type.id.inspect, Hash[values.map{ |value|
17
+ [value.id.inspect, Hash[variants.includes(:option_values).select{ |variant|
18
+ variant.option_values.select{ |val|
19
+ val.id == value.id && val.option_type_id == type.id
20
+ }.length == 1 }.map{ |v| [ v.id, { :id => v.id, :count => v.count_on_hand, :price => number_to_currency(v.price) } ] }]
21
+ ]
22
+ }]]
23
+ }]
24
+ @variant_options_hash
25
+ end
26
+
27
+ end
@@ -0,0 +1,32 @@
1
+ <%= form_for :order, :url => populate_orders_url do |f| %>
2
+ <%= hook :inside_product_cart_form do %>
3
+
4
+ <% if product_price(@product) %>
5
+ <%= hook :product_price do %>
6
+ <p class="prices">
7
+ <%= t("price") %>
8
+ <br />
9
+ <span class="price selling"><%= product_price(@product) %></span>
10
+ </p>
11
+ <% end %>
12
+ <% end %>
13
+
14
+ <%= render 'variant_options' %>
15
+
16
+ <% if @product.has_stock? || Spree::Config[:allow_backorders] %>
17
+ <%= text_field_tag (@product.has_variants? ? :quantity : "variants[#{@product.master.id}]"),
18
+ 1, :class => "title", :size => 3 %>
19
+ &nbsp;
20
+ <button type='submit' class='large primary'>
21
+ <%= image_tag('/images/add-to-cart.png') + t('add_to_cart') %>
22
+ </button>
23
+ <% else %>
24
+ <%= content_tag('strong', t('out_of_stock')) %>
25
+ <% end %>
26
+
27
+ <% end %>
28
+ <% end %>
29
+
30
+ <% content_for :head do %>
31
+ <%= javascript_include_tag 'product' %>
32
+ <% end %>
@@ -0,0 +1,43 @@
1
+ <% if @product.has_variants? %>
2
+ <div id="product-variants">
3
+ <h2><%= t('variants') %></h2>
4
+
5
+ <% index = 0 %>
6
+ <% @product.grouped_option_values.each do |type, values| %>
7
+ <div id="<%= dom_id(type) %>" class="variant-options index-<%= index %>">
8
+ <h3 class="variant-option-type"><%= type.presentation %></h3>
9
+ <ul class="variant-option-values">
10
+ <% values.each do |value| %>
11
+ <% classes = ["option-value"] %>
12
+ <% if index == 0 %>
13
+ <% variants = @variants.select{|i| i.option_values.select{|j| j.option_type_id == type.id && j == value }.length == 1 } %>
14
+ <% if variants.empty? %>
15
+ <% classes << "unavailable" %>
16
+ <% else %>
17
+ <% classes << (variants.sum(&:count_on_hand) < 1 ? "out-of-stock" : "in-stock") %>
18
+ <% end %>
19
+ <% end %>
20
+ <li>
21
+ <%= link_to content_tag(:span, value.presentation), "#", :title => value.presentation, :class => classes.join(" "), :rel => "#{type.id}-#{value.id}" %>
22
+ </li>
23
+ <% end %>
24
+ <li class="clear-option"><%= link_to "X", "#clear", :class => "clear-button clear-index-#{index}" %></li>
25
+ <li class="clear"></li>
26
+ </ul>
27
+ </div>
28
+ <% index += 1 %>
29
+ <% end %>
30
+ <%= hidden_field_tag "products[#{@product.id}]", "", :id => "variant_id", :class => "hidden" %>
31
+ <script type="text/javascript">
32
+ //<![CDATA[
33
+ var variant_options = new VariantOptions(<%== @product.variant_options_hash.to_json %>);
34
+ //]]>
35
+ </script>
36
+
37
+ </div>
38
+ <% end%>
39
+
40
+ <% content_for :head do %>
41
+ <%= stylesheet_link_tag 'variant_options' %>
42
+ <%= javascript_include_tag 'variant_options' %>
43
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%
2
+ rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : ""
3
+ rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}"
4
+ std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip"
5
+ %>
6
+ default: <%= std_opts %> features
7
+ wip: --tags @wip:3 --wip features
8
+ rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip
@@ -0,0 +1,125 @@
1
+ #===============================
2
+ # Helpers
3
+
4
+ def variant_by_descriptor(descriptor)
5
+ values = descriptor.split(" ")
6
+ values.map! { |word| OptionValue.find_by_presentation(word) rescue nil }.compact!
7
+ return if values.blank?
8
+ @product.variants.includes(:option_values).select{|i| i.option_value_ids.sort == values.map(&:id) }.first
9
+ end
10
+
11
+
12
+ #===============================
13
+ # Givens
14
+
15
+ Given /^I have a product( with variants)?$/ do |has_variants|
16
+ @product = Factory.create(has_variants ? :product_with_variants : :product)
17
+ end
18
+
19
+ Given /^the "([^"]*)" variant is out of stock$/ do |descriptor|
20
+ flunk unless @product
21
+ @variant = variant_by_descriptor(descriptor)
22
+ @variant.update_attributes(:count_on_hand => 0)
23
+ end
24
+
25
+ Given /^I have an? "([^"]*)" variant( for .*)?$/ do |descriptor, price|
26
+ price = price ? price.gsub(/[^\d\.]/, '').to_f : 10.00
27
+ values = descriptor.split(" ")
28
+ flunk unless @product && values.length == @product.option_types.length
29
+ @variant = variant_by_descriptor(descriptor)
30
+ return @variant if @variant
31
+ @product.option_type_ids.each_with_index do |otid, index|
32
+ word = values[index]
33
+ val = OptionValue.find_by_presentation(word) || Factory.create(:option_value, :option_type_id => otid, :presentation => word, :name => word.downcase)
34
+ values[index] = val
35
+ end
36
+ @variant = Factory.create(:variant, :product => @product, :option_values => values, :price => price)
37
+ @product.reload
38
+ end
39
+
40
+
41
+ #===============================
42
+ # Whens
43
+
44
+ When /^I click the (current|first|last) clear button$/ do |parent|
45
+ link = case parent
46
+ when 'first'; find(".clear-index-0")
47
+ when 'last'; find(".clear-index-#{@product.option_types.length - 1}")
48
+ else find(".clear-button:last")
49
+ end
50
+ assert_not_nil link
51
+ link.click
52
+ end
53
+
54
+ #===============================
55
+ # Thens
56
+
57
+ Then /^the source should contain the options hash$/ do
58
+ assert source.include?("VariantOptions(#{@product.variant_options_hash.to_json})")
59
+ end
60
+
61
+ Then /^I should see (enabled|disabled)+ links for the ((?!option).*) option type$/ do |state, option_type|
62
+ enabled = state == "enabled"
63
+ option_type = case option_type
64
+ when "first"; @product.option_types.first;
65
+ when "second"; @product.option_types[1];
66
+ when "last"; @product.option_types.last;
67
+ end
68
+ assert_seen option_type.presentation, :within => "#option_type_#{option_type.id} h3.variant-option-type"
69
+ option_type.option_values.each do |value|
70
+ rel = "#{option_type.id}-#{value.id}"
71
+ link = find("#option_type_#{option_type.id} a[rel='#{option_type.id}-#{value.id}']")
72
+ assert_not_nil link
73
+ assert_equal value.presentation, link.text
74
+ assert_equal "#", link.native.attribute('href').last
75
+ assert_equal "option-value #{enabled ? 'in-stock' : 'locked'}", link.native.attribute('class')
76
+ assert_equal rel, link.native.attribute('rel') # obviously!
77
+ end
78
+ end
79
+
80
+ Then /^I should have a hidden input for the selected variant$/ do
81
+ flunk unless @product
82
+ field = find("input[type=hidden]#variant_id")
83
+ assert_not_nil field
84
+ assert_equal "products[#{@product.id}]", field.native.attribute("name")
85
+ assert_equal "", field.native.attribute("value")
86
+ end
87
+
88
+ Then /^the add to cart button should be (enabled|disabled)?$/ do |state|
89
+ enabled = state == "enabled"
90
+ button = find("#cart-form button[type=submit]")
91
+ assert_equal !enabled, button.native.attribute("disabled") == "true"
92
+ end
93
+
94
+ Then /^I should see an (out-of|in)-stock link for "([^"]*)"$/ do |state, button|
95
+ in_stock = state == "in"
96
+ buttons = button.split(", ")
97
+ buttons.each do |button|
98
+ link = find_link(button)
99
+ assert_equal "option-value #{in_stock ? 'in' : 'out-of'}-stock", link.native.attribute("class")
100
+ assert_not_nil link
101
+ end
102
+ end
103
+
104
+ Then /^I should see "([^"]*)" selected within the (first|second|last) set of options$/ do |button, group|
105
+ parent = case group
106
+ when 'first'; '.variant-options.index-0'
107
+ when 'second'; '.variant-options.index-1'
108
+ when 'last'; ".variant-options.index-#{@product.option_values.length - 1}"
109
+ end
110
+ within parent do
111
+ link = find_link(button)
112
+ assert_not_nil link
113
+ assert link.native.attribute("class").include?("selected")
114
+ end
115
+ end
116
+
117
+ Then /^I should not see a selected option$/ do
118
+ assert_raises Capybara::ElementNotFound do
119
+ find(".option-value.selected")
120
+ end
121
+ end
122
+
123
+ Then /^I should be on the cart page$/ do
124
+ assert_equal cart_path, current_path
125
+ end
@@ -0,0 +1,138 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require File.expand_path("../../support/paths.rb", __FILE__)
4
+ require File.expand_path("../../support/selectors.rb", __FILE__)
5
+
6
+ def get_parent(parent)
7
+ case parent.sub(/^the\s/, '')
8
+ when "flash notice"; ".flash"
9
+ when "first set of options"; "#option_type_#{@product.option_types.first.id}"
10
+ when "second set of options"; "#option_type_#{@product.option_types[1].id}"
11
+ when "variant images label"; "#variant-images"
12
+ when "price"; ".prices .price"
13
+ else "[set-your-parent] #{parent}"
14
+ end
15
+ end
16
+
17
+
18
+ #========================================================================
19
+ # Givens
20
+
21
+ Given /^I'm on the ((?!page).*) page$/ do |path|
22
+ path = "#{path.downcase.gsub(/\s/, '_')}_path".to_sym
23
+ begin
24
+ visit send(path)
25
+ rescue
26
+ puts "#{path} could not be found!"
27
+ end
28
+ end
29
+
30
+ Given /^I'm on the ((?!page).*) page for (.*)$/ do |path, id|
31
+ case id
32
+ when "the first product"
33
+ id = @product ||= Product.last
34
+ end
35
+ path = "#{path.downcase.gsub(/\s/, '_')}_path".to_sym
36
+ begin
37
+ visit send(path, id)
38
+ rescue
39
+ puts "#{path} could not be found!"
40
+ end
41
+ end
42
+
43
+
44
+ #========================================================================
45
+ # Actions
46
+
47
+ When /^(?:|I )press "([^"]*)"$/ do |button|
48
+ # wtf button text spree!
49
+ button = "#popup_ok" if button == "OK"
50
+ click_button(button)
51
+ end
52
+
53
+ When /^I press "([^"]*)" in (.*)$/ do |button, parent|
54
+ # wtf button text spree!
55
+ button = "#popup_ok" if button == "OK"
56
+ within get_parent(parent) do
57
+ click_button(button)
58
+ end
59
+ end
60
+
61
+ When /^(?:|I )follow "([^"]*)"$/ do |link|
62
+ click_link(link)
63
+ end
64
+
65
+ When /^(?:|I )follow "([^"]*)" within (.*)$/ do |link, parent|
66
+ within get_parent(parent) do
67
+ click_link(link)
68
+ end
69
+ end
70
+
71
+
72
+ When /^I wait for (\d+) seconds?$/ do |seconds|
73
+ sleep seconds.to_f
74
+ end
75
+
76
+ When /^I confirm the popup message$/ do
77
+ find_by_id("popup_ok").click
78
+ end
79
+
80
+
81
+
82
+
83
+
84
+
85
+ #========================================================================
86
+ # Assertions
87
+
88
+ Then /^I should see "([^"]*)"$/ do |text|
89
+ assert page.has_content?(text)
90
+ end
91
+
92
+ Then /^I should see "([^"]*)" in (.*)$/ do |text, parent|
93
+ within get_parent(parent) do
94
+ assert page.has_content?(text)
95
+ end
96
+ end
97
+
98
+ Then /^I should not see "([^"]*)"$/ do |text|
99
+ assert_not page.has_content?(text)
100
+ end
101
+
102
+ Then /^I should not see "([^"]*)" in (.*)$/ do |text, parent|
103
+ within get_parent(parent) do
104
+ assert_not page.has_content?(text)
105
+ end
106
+ end
107
+
108
+ Then /^"([^"]*)" should equal "([^"]*)"$/ do |field, value|
109
+ assert_equal value, find_field(field).value
110
+ end
111
+
112
+ Then /^"([^"]*)" should have "([^"]*)" selected$/ do |field, value|
113
+ field = find_field(field)
114
+ has_match = field.text =~ /#{value}/
115
+ has_match = field.value =~ /#{value}/ unless has_match
116
+ assert has_match
117
+ end
118
+
119
+
120
+ #========================================================================
121
+ # Forms
122
+
123
+ When /^(?:|I )fill in "([^"]*)" with "([^"]*)"$/ do |field, value|
124
+ fill_in(field, :with => value)
125
+ end
126
+
127
+ When /^(?:|I )fill in the following:$/ do |fields|
128
+ fields.rows_hash.each do |name, value|
129
+ When %{I fill in "#{name}" with "#{value}"}
130
+ end
131
+ end
132
+
133
+ When /^(?:|I )select "([^"]*)" from "([^"]*)"$/ do |value, field|
134
+ select(value, :from => field)
135
+ end
136
+
137
+
138
+