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.
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