spree_enhanced_option_types 0.30.0
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.
- data/.gitignore +9 -0
- data/LICENSE +23 -0
- data/README.markdown +126 -0
- data/README.md +13 -0
- data/Rakefile +29 -0
- data/app/controllers/admin/prototypes_controller_decorator.rb +21 -0
- data/app/controllers/admin/variants_controller_decorator.rb +8 -0
- data/app/controllers/orders_controller_decorator.rb +58 -0
- data/app/helpers/variant_selection.rb +47 -0
- data/app/models/option_types_prototype.rb +2 -0
- data/app/models/option_value_decorator.rb +9 -0
- data/app/models/product_decorator.rb +40 -0
- data/app/models/prototype.rb +6 -0
- data/app/models/variant_decorator.rb +41 -0
- data/app/views/admin/option_types/_option_value_fields.html.erb +10 -0
- data/app/views/admin/option_types/edit.html.erb +34 -0
- data/app/views/admin/products/new.html.erb +58 -0
- data/app/views/admin/prototypes/_form.html.erb +57 -0
- data/app/views/admin/prototypes/_sortable_header.rhtml +3 -0
- data/app/views/admin/variants/_form.html.erb +45 -0
- data/app/views/admin/variants/index.html.erb +62 -0
- data/app/views/products/_cart_form.html.erb +37 -0
- data/app/views/products/_eot_includes.html.erb +25 -0
- data/app/views/products/_radio_2d.html.erb +82 -0
- data/app/views/products/_radio_sets.html.erb +31 -0
- data/app/views/products/_selects.html.erb +26 -0
- data/config/locales/en.yml +14 -0
- data/config/locales/ru.yml +10 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20100825095803_add_sku_to_option_values.rb +9 -0
- data/db/migrate/20101019122221_add_amount_to_option_value.rb +9 -0
- data/db/migrate/20101019122559_add_position_to_option_type_prototype.rb +9 -0
- data/db/migrate/20101019122611_set_default_for_option_value_amount.rb +9 -0
- data/doc/2d.jpg +0 -0
- data/doc/selects.jpg +0 -0
- data/doc/sets.jpg +0 -0
- data/lib/spree_enhanced_option_types.rb +50 -0
- data/lib/spree_enhanced_option_types_hooks.rb +3 -0
- data/lib/tasks/enhanced_option_types.rake +29 -0
- data/lib/tasks/install.rake +27 -0
- data/public/javascripts/enhanced-option-types.js +115 -0
- data/public/javascripts/jquery-ui-1.7.2.custom.min.js +46 -0
- data/public/javascripts/ui.core.js +519 -0
- data/public/javascripts/ui.draggable.js +766 -0
- data/public/javascripts/ui.selectable.js +257 -0
- data/public/javascripts/ui.sortable.js +1019 -0
- data/spree-enhanced-option-types.gemspec +22 -0
- metadata +132 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Redistribution and use in source and binary forms, with or without modification,
|
2
|
+
are permitted provided that the following conditions are met:
|
3
|
+
|
4
|
+
* Redistributions of source code must retain the above copyright notice,
|
5
|
+
this list of conditions and the following disclaimer.
|
6
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
7
|
+
this list of conditions and the following disclaimer in the documentation
|
8
|
+
and/or other materials provided with the distribution.
|
9
|
+
* Neither the name of the Rails Dog LLC nor the names of its
|
10
|
+
contributors may be used to endorse or promote products derived from this
|
11
|
+
software without specific prior written permission.
|
12
|
+
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
14
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
15
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
16
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
17
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
18
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
19
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
20
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
21
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
22
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
23
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.markdown
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# Enchanced Option Types
|
2
|
+
|
3
|
+
Now supporting Spree 0.30.x !
|
4
|
+
|
5
|
+
## Description
|
6
|
+
|
7
|
+
This extension enchances spree functionality when handling products with
|
8
|
+
numerous and complex variants.
|
9
|
+
|
10
|
+
Following enchancements are provided:
|
11
|
+
|
12
|
+
* Admin side:
|
13
|
+
* Selecting order of option types for prototype with drag & drop
|
14
|
+
* Optional "modifiers" for option values that can modify price of variant
|
15
|
+
* Option to generate set of variants from prototype with option types.
|
16
|
+
When option is selected during product creation, variants are created for
|
17
|
+
each combination of option values (eg. Sizes: [S,M,L], Colors: [Red, Blue]
|
18
|
+
will generate 6 variants), with prices calculated from sum of product base price
|
19
|
+
and amount of option_value modifiers.
|
20
|
+
* Option to regenerate variants when needed.
|
21
|
+
* Option to calculate variant price when creating new variant based on product
|
22
|
+
price and sum of modifiers from option values.
|
23
|
+
* Client side variant selection:
|
24
|
+
* using x select boxes (1 for each option_type)
|
25
|
+
* using x sets of radio boxes grouped in fieldsets (one fieldset for each option type)
|
26
|
+
* using 2d table of radio boxes (only when there are only 2 option types!)
|
27
|
+
* Javascript helpers:
|
28
|
+
* Instant updating of price based on variant selected using above methods
|
29
|
+
* Instand updating of number of on_hand units
|
30
|
+
* enabling/disabling options that don't have corresponding variant.
|
31
|
+
* products.js override (edge spree only) for working with variant images
|
32
|
+
|
33
|
+
Some of the functionality might not work without javascript, but much work was put
|
34
|
+
to make JS as unintrusive as possible, so It should be fairly easy excercise
|
35
|
+
to make it completelly JS independent.
|
36
|
+
|
37
|
+
## Credits
|
38
|
+
|
39
|
+
Created by Marcin Raczkowski (marcin.raczkowski@gmail.com)
|
40
|
+
|
41
|
+
2d table was inspired by Stephanie Powell [post](
|
42
|
+
http://blog.endpoint.com/2009/12/rails-ecommerce-product-optioning-in.html)
|
43
|
+
You can(and should!) read it.
|
44
|
+
|
45
|
+
## Examples
|
46
|
+
|
47
|
+

|
48
|
+

|
49
|
+

|
50
|
+
|
51
|
+
On first example you can see sets of radio boxes and modifiers in action,
|
52
|
+
also notable is separation of base price and current(variant) price, only second one is updatable.
|
53
|
+
|
54
|
+
Second one shows selects - it's much more compact then previous example,
|
55
|
+
but doesn't instantly show all options.
|
56
|
+
|
57
|
+
Thrid shows the 2d table for variant choosing.
|
58
|
+
|
59
|
+
## Instalation
|
60
|
+
|
61
|
+
Add the following to your Gemfile:
|
62
|
+
<code>gem 'spree_enhanced_option_types', :git => 'git://github.com/swistak/spree-enhanced-option-types.git'</code>
|
63
|
+
|
64
|
+
Run:
|
65
|
+
<code>bundle install</code>
|
66
|
+
|
67
|
+
and:
|
68
|
+
<code>rake spree_enhance_option_types:install</code>
|
69
|
+
|
70
|
+
and finally:
|
71
|
+
<code>rake db:migrate</code>
|
72
|
+
|
73
|
+
## Customization
|
74
|
+
|
75
|
+
User interface change is limited only to _cart_form partial from original spree.
|
76
|
+
it was separated into several subfiles to make customization and embeding in custom layouts easier.
|
77
|
+
|
78
|
+
There are no inline styles (except for 2d table, that absolutelly requires
|
79
|
+
some wire frame styles to look sane), you can either use provided _cart_form
|
80
|
+
partial as a replacement for generic spree partial, or you can roll your own and
|
81
|
+
only include one of variant choosing partials.
|
82
|
+
|
83
|
+
There are some special css classes you might be interested in:
|
84
|
+
.price.update - price field that should be updated with new price value if variant changes.
|
85
|
+
span.
|
86
|
+
|
87
|
+
Source is extensivelly documented and I recomend reading it.
|
88
|
+
|
89
|
+
## Limitations
|
90
|
+
|
91
|
+
- currently there's no way to change order of option types AFTER product is created
|
92
|
+
|
93
|
+
## TODO
|
94
|
+
|
95
|
+
- gracefull handling non-js users.
|
96
|
+
- test under other browsers then FF
|
97
|
+
|
98
|
+
## License
|
99
|
+
|
100
|
+
Copyright (c) 2009, Marcin Raczkowski
|
101
|
+
All rights reserved.
|
102
|
+
|
103
|
+
Redistribution and use in source and binary forms, with or without modification,
|
104
|
+
are permitted provided that the following conditions are met:
|
105
|
+
|
106
|
+
* Redistributions of source code must retain the above copyright notice,
|
107
|
+
this list of conditions and the following disclaimer.
|
108
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
109
|
+
this list of conditions and the following disclaimer in the documentation
|
110
|
+
and/or other materials provided with the distribution.
|
111
|
+
* Neither the name of the Marcin Raczkowski nor the names of its
|
112
|
+
contributors may be used to endorse or promote products derived from this
|
113
|
+
software without specific prior written permission.
|
114
|
+
|
115
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
116
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
117
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
118
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
119
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
120
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
121
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
122
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
123
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
124
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
125
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
126
|
+
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/packagetask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
|
7
|
+
spec = eval(File.read('enhanced_option_types.gemspec'))
|
8
|
+
|
9
|
+
Rake::GemPackageTask.new(spec) do |p|
|
10
|
+
p.gem_spec = spec
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Release to gemcutter"
|
14
|
+
task :release => :package do
|
15
|
+
require 'rake/gemcutter'
|
16
|
+
Rake::Gemcutter::Tasks.new(spec).define
|
17
|
+
Rake::Task['gem:push'].invoke
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Default Task"
|
21
|
+
task :default => [ :spec ]
|
22
|
+
|
23
|
+
require 'rspec/core/rake_task'
|
24
|
+
RSpec::Core::RakeTask.new
|
25
|
+
|
26
|
+
require 'cucumber/rake/task'
|
27
|
+
Cucumber::Rake::Task.new do |t|
|
28
|
+
t.cucumber_opts = %w{--format pretty}
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Admin::PrototypesController.class_eval do
|
2
|
+
skip_after_filter :set_habtm_associations
|
3
|
+
|
4
|
+
reorder_option_types = lambda do
|
5
|
+
set_habtm_associations
|
6
|
+
@prototype.option_types_prototypes.each do |otp|
|
7
|
+
new_position = params[:option_type][:id].index(otp.option_type_id.to_s)
|
8
|
+
# Rails get crazy when table doesn't have PrimaryID,
|
9
|
+
# updates don't work, so we have to do them using update_all
|
10
|
+
OptionTypesPrototype.update_all({ # SET
|
11
|
+
:position => new_position
|
12
|
+
}, { #WHERE
|
13
|
+
:prototype_id => otp.prototype_id,
|
14
|
+
:option_type_id => otp.option_type_id
|
15
|
+
})
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
update.after(&reorder_option_types)
|
20
|
+
create.after(&reorder_option_types)
|
21
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
OrdersController.class_eval do
|
2
|
+
|
3
|
+
def populate
|
4
|
+
@order = current_order(true)
|
5
|
+
|
6
|
+
params[:products].each do |product_id,variant_id|
|
7
|
+
if !params[:quantity].is_a?(Hash)
|
8
|
+
quantity = params[:quantity].to_i
|
9
|
+
else
|
10
|
+
quantity = params[:quantity][variant_id].to_i
|
11
|
+
end
|
12
|
+
@order.add_variant(Variant.find(variant_id), quantity) if quantity > 0
|
13
|
+
end if params[:products]
|
14
|
+
|
15
|
+
params[:variants].each do |variant_id, quantity|
|
16
|
+
quantity = quantity.to_i
|
17
|
+
@order.add_variant(Variant.find(variant_id), quantity) if quantity > 0
|
18
|
+
end if params[:variants]
|
19
|
+
|
20
|
+
params[:option_values].each_pair do |product_id, otov|
|
21
|
+
if !params[:quantity].is_a?(Array)
|
22
|
+
quantity = params[:quantity].to_i
|
23
|
+
else
|
24
|
+
quantity = params[:quantity][variant_id].to_i
|
25
|
+
end
|
26
|
+
option_value_ids = otov.map{|option_type_id, option_value_id| option_value_id}
|
27
|
+
variant = Variant.by_option_value_ids(option_value_ids, product_id).first
|
28
|
+
@order.add_variant(variant, quantity) if quantity > 0
|
29
|
+
end if params[:option_values]
|
30
|
+
|
31
|
+
redirect_to cart_path
|
32
|
+
|
33
|
+
# if quantity > 0 && variant
|
34
|
+
# if quantity > variant.on_hand
|
35
|
+
# flash[:error] = t(:stock_to_small) % [variant.on_hand]
|
36
|
+
# else
|
37
|
+
# @order.add_variant(variant, quantity)
|
38
|
+
# if @order.save
|
39
|
+
# # store order token in the session
|
40
|
+
# session[:order_token] = @order.token
|
41
|
+
# else
|
42
|
+
# flash[:error] = t(:out_of_stock)
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
# elsif quantity > 0
|
46
|
+
# flash[:error] = t(:wrong_combination)
|
47
|
+
# else
|
48
|
+
# flash[:error] = t(:wrong_quantity)
|
49
|
+
# end
|
50
|
+
# if flash[:error].blank?
|
51
|
+
# # redirect_to edit_order_url(@order)
|
52
|
+
# redirect_to cart_path
|
53
|
+
# else
|
54
|
+
# redirect_to :back
|
55
|
+
# end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module VariantSelection
|
2
|
+
# Returns array of arrays of ids of option values,
|
3
|
+
# that represent all possible combinations of option _values
|
4
|
+
# sorted by option type position in that product.
|
5
|
+
def options_values_combinations(product)
|
6
|
+
product.variants.map{|v| # we get all variants from product
|
7
|
+
# then we take all option_values
|
8
|
+
v.option_values.sort_by{|ov|
|
9
|
+
# then sort them by position of option value in product
|
10
|
+
ProductOptionType.find(:first, :conditions => {
|
11
|
+
:option_type_id => ov.option_type_id,
|
12
|
+
:product_id => product.id
|
13
|
+
}).position
|
14
|
+
}.map(&:id) # and get the id
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns hash that maps _array of ids of option values_ to _variant attributes_,
|
19
|
+
#
|
20
|
+
# eg.
|
21
|
+
#
|
22
|
+
# { [1,2,3,4] => <Variant#1> }
|
23
|
+
#
|
24
|
+
def ov_to_variant_map(product)
|
25
|
+
result = {}
|
26
|
+
product.variants.map{|v|
|
27
|
+
# we get all variants from product
|
28
|
+
# then we take all option_values
|
29
|
+
key = v.option_values.sort_by{|ov|
|
30
|
+
# then sort them by position of option value in product
|
31
|
+
ProductOptionType.find(:first, :conditions => { :option_type_id => ov.option_type_id, :product_id => product.id }).position
|
32
|
+
}.map(&:id)
|
33
|
+
result[key] = v
|
34
|
+
}
|
35
|
+
return(result)
|
36
|
+
end
|
37
|
+
|
38
|
+
# checks if there's a possible combination
|
39
|
+
#
|
40
|
+
# WARNING! This helper has equivalent on javascript side.
|
41
|
+
# If you plan to change it, make sure they both behave in the same way
|
42
|
+
def possible_combination?(all_combinations, values)
|
43
|
+
all_combinations.any?{|combination|
|
44
|
+
values.enum_for(:each_with_index).all?{|v, i| combination[i] == v}
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
Product.class_eval do
|
2
|
+
attr_accessor :create_variants
|
3
|
+
after_create :do_create_variants
|
4
|
+
|
5
|
+
has_many :option_types, :through => :product_option_types, :order => "product_option_types.position ASC"
|
6
|
+
|
7
|
+
def do_create_variants(force = false)
|
8
|
+
if (create_variants == "1" || force) && self.option_types.length > 0
|
9
|
+
generate_variant_combinations.each_with_index do |option_values, index|
|
10
|
+
sku = option_values.map(&:sku).reject(&:blank?).join("-")
|
11
|
+
sku = index+1 if sku.blank?
|
12
|
+
|
13
|
+
v = Variant.create({
|
14
|
+
:product => self,
|
15
|
+
:option_values => option_values,
|
16
|
+
:is_master => false,
|
17
|
+
:sku => self.sku.blank? ? "#{self.name.to_url[0..3]}-#{sku}" : "#{self.sku}-#{sku}"
|
18
|
+
})
|
19
|
+
v
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def default_variant
|
25
|
+
variants.first
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate_variant_combinations(option_values = nil)
|
29
|
+
option_values ||= self.option_types.map{|ot| ot.option_values}
|
30
|
+
if option_values.length == 1
|
31
|
+
option_values.first.map{|v| [v]}
|
32
|
+
else
|
33
|
+
result = []
|
34
|
+
option_values.first.each do |value|
|
35
|
+
result += generate_variant_combinations(option_values[1..-1]).map{|rv| rv.push(value) }
|
36
|
+
end
|
37
|
+
result
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
Variant.class_eval do
|
2
|
+
after_update :adjust_variant_prices, :if => lambda{|r| r.price_changed? && r.is_master}
|
3
|
+
|
4
|
+
def self.by_option_value_ids(option_value_ids, product_id)
|
5
|
+
Variant.find_by_sql(['
|
6
|
+
SELECT
|
7
|
+
option_values_variants.variant_id as id
|
8
|
+
FROM
|
9
|
+
option_values_variants, variants
|
10
|
+
WHERE
|
11
|
+
option_values_variants.option_value_id IN (?)
|
12
|
+
AND
|
13
|
+
option_values_variants.variant_id = variants.id
|
14
|
+
AND
|
15
|
+
variants.product_id = ?
|
16
|
+
GROUP BY
|
17
|
+
option_values_variants.variant_id
|
18
|
+
HAVING
|
19
|
+
COUNT(option_values_variants.variant_id) = ?',
|
20
|
+
option_value_ids, product_id, option_value_ids.length
|
21
|
+
]).map(&:reload)
|
22
|
+
end
|
23
|
+
|
24
|
+
def calculate_price(master_price=nil)
|
25
|
+
price = (master_price || product.master.price).to_i
|
26
|
+
price+= self.option_values.map{|ov| ov.amount.to_i}.sum
|
27
|
+
price > 0 ? price : 0
|
28
|
+
end
|
29
|
+
|
30
|
+
# Ensures a new variant takes the product master price when price is not supplied
|
31
|
+
def check_price
|
32
|
+
if self.price.blank?
|
33
|
+
raise "Must supply price for variant or master.price for product." if self.is_master
|
34
|
+
self.price = calculate_price
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def adjust_variant_prices
|
39
|
+
product.variants.each{|v| v.update_attribute(:price, v.calculate_price(self.price))}
|
40
|
+
end
|
41
|
+
end
|