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
@@ -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} %>