spree_variant_options 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+