spree_enhanced_option_types 0.30.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Radiobox sets](/swistak/spree-enchanced-option-types/raw/master/doc/sets.jpg)
|
48
|
+
![selects sets](/swistak/spree-enchanced-option-types/raw/master/doc/selects.jpg)
|
49
|
+
![table](/swistak/spree-enchanced-option-types/raw/master/doc/2d.jpg)
|
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
|