spree_enhanced_option_types 0.30.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +9 -0
  2. data/LICENSE +23 -0
  3. data/README.markdown +126 -0
  4. data/README.md +13 -0
  5. data/Rakefile +29 -0
  6. data/app/controllers/admin/prototypes_controller_decorator.rb +21 -0
  7. data/app/controllers/admin/variants_controller_decorator.rb +8 -0
  8. data/app/controllers/orders_controller_decorator.rb +58 -0
  9. data/app/helpers/variant_selection.rb +47 -0
  10. data/app/models/option_types_prototype.rb +2 -0
  11. data/app/models/option_value_decorator.rb +9 -0
  12. data/app/models/product_decorator.rb +40 -0
  13. data/app/models/prototype.rb +6 -0
  14. data/app/models/variant_decorator.rb +41 -0
  15. data/app/views/admin/option_types/_option_value_fields.html.erb +10 -0
  16. data/app/views/admin/option_types/edit.html.erb +34 -0
  17. data/app/views/admin/products/new.html.erb +58 -0
  18. data/app/views/admin/prototypes/_form.html.erb +57 -0
  19. data/app/views/admin/prototypes/_sortable_header.rhtml +3 -0
  20. data/app/views/admin/variants/_form.html.erb +45 -0
  21. data/app/views/admin/variants/index.html.erb +62 -0
  22. data/app/views/products/_cart_form.html.erb +37 -0
  23. data/app/views/products/_eot_includes.html.erb +25 -0
  24. data/app/views/products/_radio_2d.html.erb +82 -0
  25. data/app/views/products/_radio_sets.html.erb +31 -0
  26. data/app/views/products/_selects.html.erb +26 -0
  27. data/config/locales/en.yml +14 -0
  28. data/config/locales/ru.yml +10 -0
  29. data/config/routes.rb +3 -0
  30. data/db/migrate/20100825095803_add_sku_to_option_values.rb +9 -0
  31. data/db/migrate/20101019122221_add_amount_to_option_value.rb +9 -0
  32. data/db/migrate/20101019122559_add_position_to_option_type_prototype.rb +9 -0
  33. data/db/migrate/20101019122611_set_default_for_option_value_amount.rb +9 -0
  34. data/doc/2d.jpg +0 -0
  35. data/doc/selects.jpg +0 -0
  36. data/doc/sets.jpg +0 -0
  37. data/lib/spree_enhanced_option_types.rb +50 -0
  38. data/lib/spree_enhanced_option_types_hooks.rb +3 -0
  39. data/lib/tasks/enhanced_option_types.rake +29 -0
  40. data/lib/tasks/install.rake +27 -0
  41. data/public/javascripts/enhanced-option-types.js +115 -0
  42. data/public/javascripts/jquery-ui-1.7.2.custom.min.js +46 -0
  43. data/public/javascripts/ui.core.js +519 -0
  44. data/public/javascripts/ui.draggable.js +766 -0
  45. data/public/javascripts/ui.selectable.js +257 -0
  46. data/public/javascripts/ui.sortable.js +1019 -0
  47. data/spree-enhanced-option-types.gemspec +22 -0
  48. metadata +132 -0
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ \#*
2
+ *~
3
+ .#*
4
+ .DS_Store
5
+ .idea
6
+ .project
7
+ tmp
8
+ nbproject
9
+ *.swp
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
@@ -0,0 +1,13 @@
1
+ EnhancedOptionTypes
2
+ ===================
3
+
4
+ Introduction goes here.
5
+
6
+
7
+ Example
8
+ =======
9
+
10
+ Example goes here.
11
+
12
+
13
+ Copyright (c) 2010 [name of extension creator], released under the New BSD License
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,8 @@
1
+ Admin::VariantsController.class_eval do
2
+ def regenerate
3
+ product = Product.find_by_permalink(params[:product_id])
4
+ product.variants.delete_all
5
+ product.do_create_variants(:recreate)
6
+ redirect_to :action => :index, :product_id => params[:product_id]
7
+ end
8
+ 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,2 @@
1
+ class OptionTypesPrototype < ActiveRecord::Base
2
+ end
@@ -0,0 +1,9 @@
1
+ OptionValue.class_eval do
2
+ validates_numericality_of :amount, :allow_nil => true
3
+
4
+ after_update :adjust_variant_prices, :if => :amount_changed?
5
+
6
+ def adjust_variant_prices
7
+ variants.each{|v| v.update_attribute(:price, v.calculate_price)}
8
+ end
9
+ 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,6 @@
1
+ class Prototype < ActiveRecord::Base
2
+ has_and_belongs_to_many :properties
3
+ has_and_belongs_to_many :option_types, :order => "option_types_prototypes.position ASC"
4
+ has_many :option_types_prototypes
5
+ validates_presence_of :name
6
+ 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