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
@@ -0,0 +1,10 @@
1
+ <tr class="option_value fields" id="<%= dom_id(f.object) %>">
2
+ <td><%= f.text_field :name %></td>
3
+ <td><%= f.text_field :presentation %></td>
4
+ <td>
5
+ <%= f.text_field :amount %>
6
+ <%= t(:'number.currency.format.unit') %>
7
+ </td>
8
+ <td><%= f.text_field :sku %></td>
9
+ <td class="actions"><%= link_to_remove_fields t("remove"), f %></td>
10
+ </tr>
@@ -0,0 +1,34 @@
1
+ <%= render :partial => 'admin/shared/product_sub_menu' %>
2
+ <h1><%= t("editing_option_type") %></h1>
3
+ <%= render 'shared/error_messages', :target => @option_type %>
4
+ <%= form_for(@option_type, :url => object_url, :html => { :method => :put }) do |f| %>
5
+ <fieldset>
6
+ <legend><%= t("editing_option_type") %></legend>
7
+
8
+ <%= render :partial => "form", :locals => { :f => f } %>
9
+
10
+ <h3><%= t("option_values") %></h3>
11
+ <table class="index">
12
+ <thead>
13
+ <tr>
14
+ <th><%= t("name") %></th>
15
+ <th><%= t("display") %></th>
16
+ <th><%= t("price_modifier") %></th>
17
+ <th><%= t("sku_modifier") %></th>
18
+ <th></th>
19
+ </tr>
20
+ </thead>
21
+ <tbody id="option_values">
22
+ <tr id="none">
23
+ <td colspan="4"><%= @option_type.option_values.empty? ? t("none") : "" %></td>
24
+ </tr>
25
+ <% f.fields_for :option_values do |option_value_form| %>
26
+ <%= render "option_value_fields", :f => option_value_form %>
27
+ <% end %>
28
+ </tbody>
29
+ </table>
30
+ <%= link_to_add_fields t("add_option_value"), "tbody#option_values", f, :option_values %>
31
+
32
+ <%= render :partial => 'admin/shared/edit_resource_links' %>
33
+ </fieldset>
34
+ <% end %>
@@ -0,0 +1,58 @@
1
+ <%= render :partial => 'admin/shared/product_sub_menu' %>
2
+
3
+ <%= render "shared/error_messages", :target => @product %>
4
+
5
+ <%= form_for(:product, :url => collection_url, :html => { :multipart => true }) do |f| %>
6
+ <fieldset>
7
+
8
+ <%= f.field_container :name do %>
9
+ <%= f.label :name, t("name") %> <span class="required">*</span><br />
10
+ <%= f.text_field :name, :class => 'fullwidth title' %>
11
+ <%= f.error_message_on :name %>
12
+ <% end %>
13
+
14
+ <div class="yui-gb">
15
+ <div class="yui-u first">
16
+
17
+ <% unless @product.has_variants? %>
18
+ <%= f.field_container :sku do %>
19
+ <%= f.label :sku, t("sku") %><br />
20
+ <%= f.text_field :sku, :size => 16 %>
21
+ <%= f.error_message_on :sku %>
22
+ <% end %>
23
+ <% end %>
24
+
25
+ <p>
26
+ <%= f.label :prototype_id, t("prototype") %><br />
27
+ <%= f.collection_select :prototype_id, Prototype.all, :id, :name, :include_blank => true %>
28
+ </p>
29
+
30
+ <p>
31
+ <%= f.check_box :create_variants %>
32
+ <%= f.label :create_variants, t("create_variants") %><br />
33
+ </p>
34
+
35
+ </div>
36
+ <div class="yui-u">
37
+
38
+ <%= f.field_container :price do %>
39
+ <%= f.label :price, t("master_price")%> <span class="required">*</span><br />
40
+ <%= f.text_field :price %>
41
+ <%= f.error_message_on :price%>
42
+ <% end %>
43
+
44
+ <p>
45
+ <%= f.label :available_on, t("available_on") %><br />
46
+ <%= f.error_message_on :available_on %>
47
+ <%= f.spree_date_picker :available_on, :style=>"width:150px" %>
48
+ </p>
49
+
50
+
51
+ </div>
52
+ </div>
53
+
54
+ <%= render :partial => 'admin/shared/new_resource_links' %>
55
+
56
+ </fieldset>
57
+ <% end %>
58
+ <%= javascript_tag "datePickerController.create();" %>
@@ -0,0 +1,57 @@
1
+ <%= f.field_container :name do %>
2
+ <%= f.label :name, t("name") %><br />
3
+ <%= f.text_field :name %>
4
+ <%= f.error_message_on :name %>
5
+ <% end %>
6
+
7
+ <h3><%= t("properties") %></h3>
8
+
9
+ <ul class='checkbox-list' id='properties'>
10
+ <% Property.sorted.each do |property| %>
11
+ <% selected = if @prototype.new_record?
12
+ (params[:property] and params[:property][:id] and params[:property][:id].include?(property.id.to_s))
13
+ else
14
+ @prototype.properties.include?(property)
15
+ end %>
16
+ <li>
17
+ <label>
18
+ <%= check_box_tag "property[id][]", "#{property.id}", selected %>
19
+ <%= property.name %>
20
+ </label>
21
+ </li>
22
+ <% end %>
23
+ </ul>
24
+
25
+ <hr />
26
+
27
+ <h3><%= t("option_types") %></h3>
28
+
29
+ <ul class='checkbox-list' id='option-types'>
30
+ <% OptionType.all.sort_by{|ot|
31
+ [
32
+ @prototype.option_types.include?(ot) ? 0 : 1,
33
+ @prototype.option_types.index(ot) || ot.name
34
+ ]
35
+ }.each do |option_type| %>
36
+ <% selected = if @prototype.new_record?
37
+ (params[:option_type] and params[:option_type][:id] and params[:option_type][:id].include?(option_type.id.to_s))
38
+ else
39
+ @prototype.option_types.include?(option_type)
40
+ end %>
41
+ <li>
42
+ <%= check_box_tag "option_type[id][]", "#{option_type.id}", selected %>
43
+ <label> <%= option_type.name %> </label>
44
+ <br />
45
+ </li>
46
+ <% end %>
47
+ </ul>
48
+
49
+ <hr />
50
+ (hint: You can drag and drop option types to select their order)
51
+ <script type="text/javascript"> jQuery("#option-types").sortable() </script>
52
+ <style>
53
+ #option-types.checkbox-list li {
54
+ display: block;
55
+ float: none;
56
+ }
57
+ </style>
@@ -0,0 +1,3 @@
1
+ <% content_for(:head,
2
+ javascript_include_tag('ui.core', 'ui.draggable', 'ui.sortable')
3
+ ) %>
@@ -0,0 +1,45 @@
1
+ <div class="yui-gb">
2
+ <div class="yui-u first">
3
+
4
+ <% @product.options.each do |option| %>
5
+ <p>
6
+ <%= label :new_variant, option.option_type.presentation %><br />
7
+ <% if @variant.new_record? %>
8
+ <%= select(:new_variant, option.option_type.presentation,
9
+ option.option_type.option_values.collect {|ov| [ ov.presentation, ov.id ] },
10
+ {})
11
+ %>
12
+ <% else %>
13
+ <% opt = @variant.option_values.detect {|o| o.option_type == option.option_type }.presentation %>
14
+ <%= text_field(:new_variant, option.option_type.presentation, :value => opt, :disabled => 'disabled') %>
15
+ <% end %>
16
+ </p>
17
+ <% end %>
18
+
19
+ <p><%= f.label :sku, t("sku") %><br />
20
+ <%= f.text_field :sku %></p>
21
+
22
+ <p>
23
+ <%= f.label :price, t("price") %>:<br />
24
+ <%= f.text_field :price, :value => number_with_precision(@variant.price, :precision => 2) %>
25
+ <br/>
26
+ (* if left empty - price will be calculated automatically from option values and master price)
27
+ </p>
28
+
29
+
30
+ <p><%= f.label :cost_price, t("cost_price") %>:<br />
31
+ <%= f.text_field :cost_price, :value => number_with_precision(@variant.cost_price, :precision => 2) %></p>
32
+
33
+ <% if Spree::Config[:track_inventory_levels] %>
34
+ <p><%= f.label :on_hand, t("on_hand") %>: <br />
35
+ <%= f.text_field :on_hand %></p>
36
+ <% end %>
37
+ </div>
38
+ <div class="yui-u">
39
+
40
+ <% Variant.additional_fields.select{|af| af[:only].nil? || af[:only].include?(:variant) }.each do |field| %>
41
+ <%= render :partial => "admin/shared/additional_field", :locals => {:field => field, :f => f} %>
42
+ <% end %>
43
+
44
+ </div>
45
+ </div>
@@ -0,0 +1,62 @@
1
+ <div class='toolbar'>
2
+ <ul class='actions'>
3
+ <li id="regenerate_variants_link">
4
+ <%= button_link_to t(:regenerate), {:action => :regenerate, :product_id => params[:product_id]},
5
+ {:method => :post, :icon => 'exclamation', :style => "float: right", :confirm => t(:regenerate_confirm)} %>
6
+ </li>
7
+ </ul>
8
+ <br class='clear' />
9
+ </div>
10
+
11
+
12
+ <%= render :partial => 'admin/shared/product_sub_menu' %>
13
+
14
+ <%= render :partial => 'admin/shared/product_tabs', :locals => {:current => "Variants"} %>
15
+
16
+ <table class="index">
17
+ <tr>
18
+ <th><%= t("options") %></th>
19
+ <th><%= t("price") %></th>
20
+ <th><%= t("sku") %></th>
21
+ <% Variant.additional_fields.select{|f| f[:only].nil? || f[:only].include?(:variant) }.each do |field| %>
22
+ <th><%= t("activerecord.attributes.variant." + field[:name].downcase, :default => field[:name].titleize) %></th>
23
+ <% end %>
24
+ <th><%= t("on_hand") %></th>
25
+ <th><%= t("action") %></th>
26
+ </tr>
27
+ <% @variants.each do |variant| %>
28
+ <!-- you can skip variant with no options: that's just the default variant that all products have -->
29
+ <% next if variant.option_values.empty? %>
30
+ <tr id="<%= dom_id(variant) %>" <%= 'style="color:red;"' if variant.deleted? %>>
31
+ <td><%= variant_options variant %></td>
32
+ <td><%= variant.price %></td>
33
+ <td><%= variant.sku %></td>
34
+ <% Variant.additional_fields.select{|f| f[:only].nil? || f[:only].include?(:variant) }.each do |field| %>
35
+ <td><%= variant[field[:name].gsub(" ", "_").downcase] %></td>
36
+ <% end %>
37
+ <td><%= variant.on_hand %></td>
38
+ <td valign="top">
39
+ <%= link_to_edit(variant) unless variant.deleted? %>
40
+ &nbsp;
41
+ <%= link_to_delete(variant) unless variant.deleted? %>
42
+ </td>
43
+ </tr>
44
+ <% end %>
45
+ <% unless @product.has_variants? %>
46
+ <tr><td colspan="5"><%= t("none") %>.</td></tr>
47
+ <% end %>
48
+ </table>
49
+
50
+ <% if @product.options.empty? %>
51
+ <p>
52
+ <%= t("to_add_variants_you_must_first_define") %> <%= link_to t("option_types"), selected_admin_product_option_types_url(@product) %>
53
+ </p>
54
+ <% else %>
55
+ <div id="new_variant"></div>
56
+ <br/>
57
+ <p id="new_var_link">
58
+ <%= link_to icon('add') + ' ' + t("new_variant"), new_admin_product_variant_url(@product), :remote => :true, 'data-update' => 'new_variant', :class => 'iconlink' %>
59
+ &nbsp;|&nbsp;<%= link_to @deleted.blank? ? t("show_deleted") : t("show_active"), admin_product_variants_url(@product, :deleted => @deleted.blank? ? "on" : "off") %>
60
+ </p>
61
+ <%= image_tag "spinner.gif", :plugin=>"spree", :style => "display:none", :id => 'busy_indicator' %>
62
+ <% end %>
@@ -0,0 +1,37 @@
1
+ <%= form_for :order, :url => populate_orders_url do |f| %>
2
+
3
+ <%= hook :inside_product_cart_form do %>
4
+
5
+ <% if product_price(@product) %>
6
+ <%= hook :product_price do %>
7
+ <p class="prices">
8
+ <%= t("price") %>
9
+ <br />
10
+ <span class="price selling update"><%= product_price(@product) %></span>
11
+ </p>
12
+ <% end %>
13
+ <% end %>
14
+
15
+ <% if @product.has_variants? %>
16
+ <div id="product-variants">
17
+ <%= render :partial => 'radio_sets', :locals => {:f => f} %>
18
+ </div>
19
+ <% end%>
20
+ <% if @product.has_stock? || Spree::Config[:allow_backorders] %>
21
+ <%= text_field_tag (@product.has_variants? ? :quantity : "variants[#{@product.master.id}]"),
22
+ 1, :class => "title", :size => 3 %>
23
+ &nbsp;
24
+ <button type='submit' class='large primary'>
25
+ <%= image_tag('/images/add-to-cart.png') + t('add_to_cart') %>
26
+ </button>
27
+ <% else %>
28
+ <%= content_tag('strong', t('out_of_stock')) %>
29
+ <% end %>
30
+
31
+ <% end %>
32
+ <% end %>
33
+
34
+ <% content_for :head do %>
35
+ <%= javascript_include_tag 'product' %>
36
+ <% end %>
37
+
@@ -0,0 +1,25 @@
1
+ <%
2
+ js_ov_to_variant_map = {}
3
+ variant_attributes_hash = {}
4
+
5
+ ov_to_variant_map(@product).each_pair do |k,v|
6
+ js_ov_to_variant_map[k.join(',')] = v.id
7
+ variant_attributes_hash[v.id] = v.attributes.merge(:on_hand => v.on_hand, :description => variant_options(v))
8
+ end
9
+
10
+ %>
11
+ <script type="text/javascript">
12
+ // set of all possible combination of option values
13
+ var ov_combinations = <%= ov_combinations.to_json.html_safe %>;
14
+ // maps variant attributes to variant id
15
+ var ov_to_variant = <%= js_ov_to_variant_map.to_json.html_safe %>;
16
+ // mapping from variant id to variant attributes
17
+ var variant_attributes = <%= variant_attributes_hash.to_json.html_safe %>;
18
+ </script>
19
+ <%#
20
+ WARNING! This scripts are inlined here on purpose! for ease of embeding and customization.
21
+ If you plan to extract them and put them into header, don't.
22
+ And if you really really really want to please make sure they are wrapped into
23
+ document ready event (it's not needed here as we put script AFTER everything we need is in DOM and loaded)
24
+ %>
25
+ <%= javascript_include_tag('enhanced-option-types.js') %>
@@ -0,0 +1,82 @@
1
+ <% ov_combinations = options_values_combinations(@product) %>
2
+ <% ov_to_vhash = ov_to_variant_map(@product) %>
3
+
4
+ <% x_option_type, y_option_type = *@product.option_types.uniq[0..1] %>
5
+ <% width = x_option_type.option_values.length %>
6
+ <% height = y_option_type.option_values.length %>
7
+ <% default = @product.default_variant %>
8
+
9
+ <table class="t2d">
10
+ <thead>
11
+ <tr>
12
+ <th>&nbsp;</th>
13
+ <th>&nbsp;</th>
14
+ <th colspan="<%= width %>" class="x-axis"><%= x_option_type.presentation %></th>
15
+ </tr>
16
+ </thead>
17
+
18
+ <tbody>
19
+ <tr>
20
+ <td rowspan="<%= height+1 %>" class="y-axis">
21
+ <%== y_option_type.presentation.split(//u).join("<br />") %>
22
+ </td>
23
+ <td class="option top left">&nbsp;</td>
24
+ <% x_option_type.option_values.each do |ov| %>
25
+ <td class="option top"><%== ov.presentation.split(//u)[0..4].join("<br />") %></td>
26
+ <% end %>
27
+ </tr>
28
+
29
+ <% y_option_type.option_values.each_with_index do |yov, i| %>
30
+ <tr>
31
+ <td class="option left"><%= yov.presentation %></td>
32
+ <% x_option_type.option_values.each do |xov| %>
33
+ <td>
34
+ <%=
35
+ v = ov_to_vhash[[xov.id, yov.id]]
36
+ radio_button_tag(
37
+ "products[#{@product.id}]",
38
+ v ? v.id : "",
39
+ v && (v == default),
40
+ :disabled => !(v && (v.in_stock || Spree::Config[:allow_backorders]))
41
+ ) %>
42
+ </td>
43
+ <% end %>
44
+ </tr>
45
+ <% end %>
46
+ </tbody>
47
+ </table>
48
+
49
+ <%= render :partial => 'eot_includes', :locals => {:ov_combinations => ov_combinations} %>
50
+
51
+ <style type="text/css">
52
+ table.t2d {
53
+
54
+ }
55
+
56
+ table.t2d td{
57
+ width: 30px;
58
+ height: 30px;
59
+ vertical-align: middle;
60
+ text-align: center;
61
+ margin: 0px;
62
+ padding: 0px;
63
+ }
64
+
65
+ table.t2d th{
66
+ text-align: center;
67
+ }
68
+
69
+ table.t2d td.option.left{
70
+ text-align: right;
71
+ border-bottom: 1px solid #444;
72
+ }
73
+
74
+ table.t2d td.option.top{
75
+ border-right: 1px solid #444;
76
+ }
77
+
78
+ table.t2d td.y-axis {
79
+ font-weight: bold;
80
+ border: none;
81
+ }
82
+ </style>
@@ -0,0 +1,31 @@
1
+ <% ov_combinations = options_values_combinations(@product) %>
2
+
3
+ <% default_values = @product.default_variant.option_values.group_by{|ov| ov.option_type_id} if @product.default_variant %>
4
+ <% selected_values = [] %>
5
+ <% @product.option_types.uniq.each do |option_type|
6
+ default_value = default_values[option_type.id].first;
7
+ %>
8
+ <fieldset>
9
+ <legend> <%= option_type.presentation %> </legend>
10
+ <ul><% option_type.option_values.each_with_index do |option_value, i| %>
11
+ <li>
12
+ <%= radio_button_tag(
13
+ "option_values[#{@product.id}][#{option_type.id}]",
14
+ option_value.id,
15
+ option_value == default_value,
16
+ :disabled => !possible_combination?(ov_combinations, selected_values+[option_value.id]),
17
+ :id => "ov_"+option_value.id.to_s, :class=> "option-value"
18
+ ) %>
19
+ <%= option_value.presentation %>
20
+ <% unless option_value.amount.blank? %>
21
+ <span class="modifier" style="white-space:nowrap;">
22
+ (<%= "#{option_value.amount.sign_symbol} #{number_with_precision(option_value.amount.abs, :precision => 2)}" %>)
23
+ </span>
24
+ <% end %>
25
+ </li>
26
+ <% end %></ul>
27
+ </fieldset>
28
+ <% selected_values << default_value.id;
29
+ end %>
30
+
31
+ <%= render :partial => 'eot_includes', :locals => {:ov_combinations => ov_combinations} %>