solidus_related_products 1.0.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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.hound.yml +25 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +8 -0
  6. data/.travis.yml +15 -0
  7. data/CONTRIBUTING.md +81 -0
  8. data/Gemfile +6 -0
  9. data/Guardfile +10 -0
  10. data/LICENSE.md +26 -0
  11. data/README.md +80 -0
  12. data/Rakefile +15 -0
  13. data/app/assets/javascripts/spree/backend/solidus_related_products.js +1 -0
  14. data/app/assets/javascripts/spree/frontend/solidus_related_products.js +1 -0
  15. data/app/assets/stylesheets/spree/backend/solidus_related_products.css +3 -0
  16. data/app/assets/stylesheets/spree/frontend/solidus_related_products.css +3 -0
  17. data/app/controllers/spree/admin/products_controller_decorator.rb +10 -0
  18. data/app/controllers/spree/admin/relation_types_controller.rb +6 -0
  19. data/app/controllers/spree/admin/relations_controller.rb +80 -0
  20. data/app/controllers/spree/api/relations_controller.rb +78 -0
  21. data/app/models/spree/calculator/related_product_discount.rb +50 -0
  22. data/app/models/spree/product_decorator.rb +106 -0
  23. data/app/models/spree/relation.rb +7 -0
  24. data/app/models/spree/relation_type.rb +6 -0
  25. data/app/overrides/add_product_relation_admin_sub_menu_tab.rb +6 -0
  26. data/app/overrides/add_related_product_admin_tabs.rb +6 -0
  27. data/app/views/spree/admin/products/_related_products.html.erb +3 -0
  28. data/app/views/spree/admin/products/_related_products_table.html.erb +42 -0
  29. data/app/views/spree/admin/products/related.html.erb +70 -0
  30. data/app/views/spree/admin/relation_types/_form.html.erb +19 -0
  31. data/app/views/spree/admin/relation_types/edit.html.erb +14 -0
  32. data/app/views/spree/admin/relation_types/index.html.erb +44 -0
  33. data/app/views/spree/admin/relation_types/new.html.erb +14 -0
  34. data/app/views/spree/admin/relations/create.js.erb +5 -0
  35. data/app/views/spree/admin/relations/destroy.js.erb +1 -0
  36. data/app/views/spree/api/relations/show.v1.rabl +2 -0
  37. data/bin/rails +7 -0
  38. data/config/locales/cs.yml +18 -0
  39. data/config/locales/de.yml +18 -0
  40. data/config/locales/en.yml +18 -0
  41. data/config/locales/es.yml +18 -0
  42. data/config/locales/fr.yml +18 -0
  43. data/config/locales/it.yml +18 -0
  44. data/config/locales/nl.yml +18 -0
  45. data/config/locales/pl.yml +18 -0
  46. data/config/locales/pt-BR.yml +18 -0
  47. data/config/locales/ru.yml +18 -0
  48. data/config/locales/sv.yml +18 -0
  49. data/config/routes.rb +24 -0
  50. data/db/migrate/20100308090631_create_relation_types.rb +14 -0
  51. data/db/migrate/20100308092101_create_relations.rb +14 -0
  52. data/db/migrate/20100324123835_add_discount_to_relation.rb +9 -0
  53. data/db/migrate/20111129044813_prefixing_tables_with_spree.rb +6 -0
  54. data/db/migrate/20120208144454_update_relation_types.rb +9 -0
  55. data/db/migrate/20120623014337_update_relations.rb +11 -0
  56. data/db/migrate/20130727004612_add_position_to_spree_relations.rb +5 -0
  57. data/lib/generators/solidus_related_products/install/install_generator.rb +20 -0
  58. data/lib/solidus_related_products.rb +7 -0
  59. data/lib/solidus_related_products/engine.rb +24 -0
  60. data/lib/solidus_related_products/version.rb +18 -0
  61. data/solidus_related_products.gemspec +45 -0
  62. data/spec/controllers/spree/admin/products_controller_decorator_spec.rb +20 -0
  63. data/spec/controllers/spree/admin/relations_controller_spec.rb +96 -0
  64. data/spec/controllers/spree/api/relations_controller_spec.rb +98 -0
  65. data/spec/factories/relation_factory.rb +7 -0
  66. data/spec/factories/relation_type_factory.rb +6 -0
  67. data/spec/features/spree/admin/product_relation_spec.rb +86 -0
  68. data/spec/features/spree/admin/relation_types_spec.rb +97 -0
  69. data/spec/models/spree/calculator/related_product_discount_spec.rb +48 -0
  70. data/spec/models/spree/product_spec.rb +129 -0
  71. data/spec/models/spree/relation_spec.rb +13 -0
  72. data/spec/models/spree/relation_type_spec.rb +18 -0
  73. data/spec/spec_helper.rb +48 -0
  74. data/spec/support/capybara.rb +20 -0
  75. data/spec/support/database_cleaner.rb +23 -0
  76. data/spec/support/factory_girl.rb +7 -0
  77. data/spec/support/spree.rb +18 -0
  78. metadata +388 -0
@@ -0,0 +1,50 @@
1
+ module Spree
2
+ class Calculator::RelatedProductDiscount < Spree::Calculator
3
+ def self.description
4
+ Spree.t(:related_product_discount)
5
+ end
6
+
7
+ def compute(object)
8
+ if object.is_a?(Array)
9
+ return if object.empty?
10
+ order = object.first.order
11
+ else
12
+ order = object
13
+ end
14
+
15
+ return unless eligible?(order)
16
+ total = order.line_items.inject(0) do |sum, line_item|
17
+ relations = Spree::Relation.where(*discount_query(line_item))
18
+ discount_applies_to = relations.map {|rel| rel.related_to.master }
19
+
20
+ order.line_items.each do |li|
21
+ next unless discount_applies_to.include? li.variant
22
+ discount = relations.detect { |rel| rel.related_to.master == li.variant }.discount_amount
23
+ sum += if li.quantity < line_item.quantity
24
+ (discount * li.quantity)
25
+ else
26
+ (discount * line_item.quantity)
27
+ end
28
+ end
29
+
30
+ sum
31
+ end
32
+
33
+ total
34
+ end
35
+
36
+ def eligible?(order)
37
+ order.line_items.any? do |line_item|
38
+ Spree::Relation.exists?(discount_query(line_item))
39
+ end
40
+ end
41
+
42
+ def discount_query(line_item)
43
+ [
44
+ 'discount_amount <> 0.0 AND relatable_type = ? AND relatable_id = ?',
45
+ 'Spree::Product',
46
+ line_item.variant.product.id
47
+ ]
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,106 @@
1
+ Spree::Product.class_eval do
2
+ has_many :relations, -> { order(:position) }, as: :relatable
3
+
4
+ # When a Spree::Product is destroyed, we also want to destroy all Spree::Relations
5
+ # "from" it as well as "to" it.
6
+ after_destroy :destroy_product_relations
7
+
8
+ # Returns all the Spree::RelationType's which apply_to this class.
9
+ def self.relation_types
10
+ Spree::RelationType.where(applies_to: to_s).order(:name)
11
+ end
12
+
13
+ # The AREL Relations that will be used to filter the resultant items.
14
+ #
15
+ # By default this will remove any items which are deleted, or not yet available.
16
+ #
17
+ # You can override this method to fine tune the filter. For example,
18
+ # to only return Spree::Product's with more than 2 items in stock, you could
19
+ # do the following:
20
+ #
21
+ # def self.relation_filter
22
+ # set = super
23
+ # set.where('spree_products.count_on_hand >= 2')
24
+ # end
25
+ #
26
+ # This could also feasibly be overridden to sort the result in a
27
+ # particular order, or restrict the number of items returned.
28
+ def self.relation_filter
29
+ where('spree_products.deleted_at' => nil)
30
+ .where('spree_products.available_on IS NOT NULL')
31
+ .where('spree_products.available_on <= ?', Time.now)
32
+ .references(self)
33
+ end
34
+
35
+ # Decides if there is a relevant Spree::RelationType related to this class
36
+ # which should be returned for this method.
37
+ #
38
+ # If so, it calls relations_for_relation_type. Otherwise it passes
39
+ # it up the inheritance chain.
40
+ def method_missing(method, *args)
41
+ # Fix for Ruby 1.9
42
+ raise NoMethodError if method == :to_ary
43
+
44
+ relation_type = find_relation_type(method)
45
+ if relation_type.nil?
46
+ super
47
+ else
48
+ relations_for_relation_type(relation_type)
49
+ end
50
+ end
51
+
52
+ def has_related_products?(relation_method)
53
+ find_relation_type(relation_method).present?
54
+ end
55
+
56
+ def destroy_product_relations
57
+ # First we destroy relationships "from" this Product to others.
58
+ relations.destroy_all
59
+ # Next we destroy relationships "to" this Product.
60
+ Spree::Relation.where(related_to_type: self.class.to_s).where(related_to_id: id).destroy_all
61
+ end
62
+
63
+ private
64
+
65
+ def find_relation_type(relation_name)
66
+ self.class.relation_types.detect { |rt| format_name(rt.name) == format_name(relation_name) }
67
+ rescue ActiveRecord::StatementInvalid
68
+ # This exception is throw if the relation_types table does not exist.
69
+ # And this method is getting invoked during the execution of a migration
70
+ # from another extension when both are used in a project.
71
+ nil
72
+ end
73
+
74
+ # Returns all the Products that are related to this record for the given RelationType.
75
+ #
76
+ # Uses the Relations to find all the related items, and then filters
77
+ # them using +Product.relation_filter+ to remove unwanted items.
78
+ def relations_for_relation_type(relation_type)
79
+ # Find all the relations that belong to us for this RelationType, ordered by position
80
+ related_ids = relations.where(relation_type_id: relation_type.id).order(:position).select(:related_to_id)
81
+
82
+ # Construct a query for all these records
83
+ result = self.class.where(id: related_ids)
84
+
85
+ # Merge in the relation_filter if it's available
86
+ result = result.merge(self.class.relation_filter) if relation_filter
87
+
88
+ # make sure results are in same order as related_ids array (position order)
89
+ if result.present?
90
+ result.where(id: related_ids).order(:position)
91
+ end
92
+
93
+ result
94
+ end
95
+
96
+ # Simple accessor for the class-level relation_filter.
97
+ # Could feasibly be overloaded to filter results relative to this
98
+ # record (eg. only higher priced items)
99
+ def relation_filter
100
+ self.class.relation_filter
101
+ end
102
+
103
+ def format_name(name)
104
+ name.to_s.downcase.gsub(' ', '_').pluralize
105
+ end
106
+ end
@@ -0,0 +1,7 @@
1
+ class Spree::Relation < ActiveRecord::Base
2
+ belongs_to :relation_type
3
+ belongs_to :relatable, polymorphic: true
4
+ belongs_to :related_to, polymorphic: true
5
+
6
+ validates :relation_type, :relatable, :related_to, presence: true
7
+ end
@@ -0,0 +1,6 @@
1
+ class Spree::RelationType < ActiveRecord::Base
2
+ has_many :relations, dependent: :destroy
3
+
4
+ validates :name, :applies_to, presence: true
5
+ validates :name, uniqueness: { case_sensitive: false }
6
+ end
@@ -0,0 +1,6 @@
1
+ Deface::Override.new(
2
+ virtual_path: 'spree/admin/shared/_product_sub_menu',
3
+ name: 'add_product_relation_admin_sub_menu_tab',
4
+ insert_bottom: '[data-hook="admin_product_sub_tabs"]',
5
+ text: '<%= tab :relation_types %>'
6
+ )
@@ -0,0 +1,6 @@
1
+ Deface::Override.new(
2
+ virtual_path: 'spree/admin/shared/_product_tabs',
3
+ name: 'add_related_products_admin_tab',
4
+ insert_bottom: '[data-hook="admin_product_tabs"]',
5
+ partial: 'spree/admin/products/related_products'
6
+ )
@@ -0,0 +1,3 @@
1
+ <li<%== ' class="active"' if current == "Related Products" %>>
2
+ <%= link_to_with_icon 'resize-small', Spree.t(:related_products), related_admin_product_url(@product) %>
3
+ </li>
@@ -0,0 +1,42 @@
1
+ <table class="index sortable" data-hook="products_table" data-sortable-link="<%= update_positions_admin_product_relations_url(@product) %>">
2
+ <colgroup>
3
+ <col style="width: 5%" />
4
+ <col style="width: 35%" />
5
+ <col style="width: 35%" />
6
+ <col style="width: 20%" />
7
+ <col style="width: 5%" />
8
+ </colgroup>
9
+ <thead>
10
+ <tr data-hook="products_header">
11
+ <th></th>
12
+ <th><%= Spree.t(:name) %></th>
13
+ <th><%= Spree.t(:type) %></th>
14
+ <th><%= Spree.t(:discount_amount) %></th>
15
+ <th class="actions"></th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <% product.relations.each do |relation| %>
20
+ <tr id="<%= spree_dom_id relation %>" data-hook="products_row">
21
+ <td class="handle move-handle">
22
+ <span class="icon icon-move handle"></span>
23
+ </td>
24
+ <td><%= link_to relation.related_to.name, relation.related_to %></td>
25
+ <td><%= relation.relation_type.name %></td>
26
+ <td>
27
+ <%= form_for relation, url: admin_product_relation_path(relation.relatable, relation) do |f| %>
28
+ <div class="input-group">
29
+ <%= f.text_field :discount_amount, class: 'form-control text-center' %>
30
+ <span class="input-group-btn">
31
+ <%= f.button Spree.t(:update), type: 'submit', class: 'btn btn-primary' %>
32
+ </span>
33
+ </div>
34
+ <% end %>
35
+ </td>
36
+ <td class="actions">
37
+ <%= link_to_delete relation, url: admin_product_relation_url(relation.relatable, relation), no_text: true %>
38
+ </td>
39
+ </tr>
40
+ <% end %>
41
+ </tbody>
42
+ </table>
@@ -0,0 +1,70 @@
1
+ <%= render partial: 'spree/admin/shared/product_tabs', locals: { current: 'Related Products' } %>
2
+
3
+ <%= csrf_meta_tag %>
4
+
5
+ <% if @relation_types.empty? %>
6
+ <div class="alert alert-warning no-objects-found">
7
+ <%= Spree.t(:no_relation_types) %>
8
+ </div>
9
+ <% else %>
10
+ <div id="add-line-item">
11
+ <fieldset>
12
+ <legend><%= Spree.t(:add_related_product) %></legend>
13
+ <div class="row">
14
+ <div id="related_product_name" class="col-md-5">
15
+ <div class="form-group">
16
+ <%= label_tag :add_variant_name, Spree.t(:name_or_sku_short) %>
17
+ <%= hidden_field_tag :add_variant_name, '', class: 'variant_autocomplete', style: "display: block !important;" %>
18
+ </div>
19
+ </div>
20
+ <div id="related_product_type" class="col-md-4">
21
+ <div class="form-group">
22
+ <%= label_tag :add_type, Spree.t(:type) %>
23
+ <%= select_tag :add_type, options_for_select(@relation_types.map { |rt| [rt.name, rt.id] }), class: 'select2' %>
24
+ </div>
25
+ </div>
26
+ <div id="related_product_discount" class="col-md-3">
27
+ <div class="form-group">
28
+ <%= label_tag :add_discount, Spree.t(:discount_amount) %>
29
+ <div class="input-group">
30
+ <%= text_field_tag :add_discount, 0.0, class: 'form-control text-center' %>
31
+ <span class="input-group-btn">
32
+ <%= button_link_to Spree.t(:add), admin_product_relations_url(@product), icon: 'add', class: 'btn-success', id: 'add_related_product', data: { update: 'products-table-wrapper' } %>
33
+ </span>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </fieldset>
39
+ </div>
40
+
41
+ <div id="products-table-wrapper">
42
+ <%= render 'related_products_table', product: @product %>
43
+ </div>
44
+
45
+ <%= content_for :head do %>
46
+ <script type="text/javascript">
47
+ var expand_variants = false;
48
+ $(document).on('click', '#add_related_product', function(){
49
+ if($('#add_variant_name').val() == '') { return false; }
50
+ update_target = $(this).data('update');
51
+ $.ajax({
52
+ dataType: 'script',
53
+ url: this.href,
54
+ type: 'POST',
55
+ data: {
56
+ 'relation[related_to_type]' : 'Product',
57
+ 'relation[related_to_id]': $('#add_variant_name').val(),
58
+ 'relation[relation_type_id]': $('#add_type').val(),
59
+ 'relation[discount_amount]' : $('#add_discount').val()
60
+ }
61
+ });
62
+ return false;
63
+ });
64
+
65
+ $(function () {
66
+ $('#add_variant_name.variant_autocomplete').variantAutocomplete();
67
+ });
68
+ </script>
69
+ <% end %>
70
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <div data-hook="admin_relation_type_form_fields" class="row">
2
+ <div class="col-md-12">
3
+ <div data-hook="name" class="form-group">
4
+ <%= f.label :name, Spree.t(:name) %> <span class="required">*</span>
5
+ <%= f.text_field :name, class: 'form-control' %>
6
+ <%= error_message_on :relation_type, :name %>
7
+ </div>
8
+ <div data-hook="applies_to" class="form-group">
9
+ <%= f.label :applies_to, Spree.t(:applies_to) %> <span class="required">*</span>
10
+ <%= f.text_field :applies_to, value: f.object.nil? ? 'Spree::Product' : f.object.applies_to, class: 'form-control' %>
11
+ <%= error_message_on :relation_type, :applies_to %>
12
+ </div>
13
+ <div data-hook="description" class="form-group">
14
+ <%= f.label :description, Spree.t(:description) %><br />
15
+ <%= f.text_area :description, rows: 4, class: 'form-control' %>
16
+ <%= error_message_on :relation_type, :description %>
17
+ </div>
18
+ </div>
19
+ </div>
@@ -0,0 +1,14 @@
1
+ <% content_for :page_title do %>
2
+ <%= Spree.t(:editing_resource, resource: Spree::RelationType.model_name.human) %>
3
+ <% end %>
4
+
5
+ <% content_for :page_actions do %>
6
+ <%= button_link_to Spree.t(:back_to_resource_list, resource: Spree::RelationType.model_name.human), spree.admin_relation_types_path, class: 'btn-primary', icon: 'arrow-left' %>
7
+ <% end %>
8
+
9
+ <%= form_for [:admin, @relation_type] do |f| %>
10
+ <fieldset>
11
+ <%= render 'form', f: f %>
12
+ <%= render 'spree/admin/shared/edit_resource_links' %>
13
+ </fieldset>
14
+ <% end %>
@@ -0,0 +1,44 @@
1
+ <% content_for :page_title do %>
2
+ <%= Spree::RelationType.model_name.human(count: :many) %>
3
+ <% end %>
4
+
5
+ <% content_for :page_actions do %>
6
+ <%= button_link_to Spree.t(:new_relation_type), new_object_url, class: 'btn-primary', icon: 'add', id: 'admin_new_relation_type' %>
7
+ <% end %>
8
+
9
+ <% if @relation_types.any? %>
10
+ <table class="index" id="listing_relation_types" data-hook>
11
+ <colgroup>
12
+ <col style="width: 20%" />
13
+ <col style="width: 20%" />
14
+ <col style="width: 40%" />
15
+ <col style="width: 20%" />
16
+ </colgroup>
17
+ <thead>
18
+ <tr data-hook="admin_relation_types_index_headers">
19
+ <th><%= Spree.t(:name) %></th>
20
+ <th><%= Spree.t(:applies_to) %></th>
21
+ <th><%= Spree.t(:description) %></th>
22
+ <th class="actions" data-hook="admin_pages_index_header_actions"></th>
23
+ </tr>
24
+ </thead>
25
+ <tbody>
26
+ <% @relation_types.each do |relation_type| %>
27
+ <tr id="<%= spree_dom_id relation_type %>" data-hook="admin_relation_types_index_row" class="<%= cycle('odd', 'even') %>">
28
+ <td><%= relation_type.name %></td>
29
+ <td><%= relation_type.applies_to %></td>
30
+ <td><%= relation_type.description %></td>
31
+ <td class="actions" data-hook="admin_relation_types_index_row_actions">
32
+ <%= link_to_edit relation_type, no_text: true %>
33
+ <%= link_to_delete relation_type, no_text: true %>
34
+ </td>
35
+ </tr>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
39
+ <% else %>
40
+ <div class="alert alert-warning no-objects-found">
41
+ <%= Spree.t(:no_resource_found, resource: Spree::RelationType.model_name.human(count: :many)) %>,
42
+ <%= link_to Spree.t(:add_one), spree.new_admin_relation_type_path %>!
43
+ </div>
44
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <% content_for :page_title do %>
2
+ <%= Spree.t(:new_relation_type) %>
3
+ <% end %>
4
+
5
+ <% content_for :page_actions do %>
6
+ <%= button_link_to Spree.t(:back_to_resource_list, resource: Spree::RelationType.model_name.human), spree.admin_relation_types_path, class: 'btn-primary', icon: 'arrow-left' %>
7
+ <% end %>
8
+
9
+ <%= form_for :relation_type, url: collection_url do |f| %>
10
+ <fieldset>
11
+ <%= render 'form', f: f %>
12
+ <%= render 'spree/admin/shared/new_resource_links' %>
13
+ </fieldset>
14
+ <% end %>
@@ -0,0 +1,5 @@
1
+ $('#products-table-wrapper').html('<%= escape_javascript(render "spree/admin/products/related_products_table", product: @product) %>');
2
+ $('#add_product_name').val('');
3
+ $('#add_variant_id').val('');
4
+ $('#add_quantity').val(1);
5
+ $('.actions a').tooltip();
@@ -0,0 +1 @@
1
+ $('#products-table-wrapper').html('<%= escape_javascript(render "spree/admin/products/related_products_table", product: @product) %>');
@@ -0,0 +1,2 @@
1
+ object @relation
2
+ attributes *Spree::Relation.column_names
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
4
+ ENGINE_PATH = File.expand_path('../../lib/spree_related_products/engine', __FILE__)
5
+
6
+ require 'rails/all'
7
+ require 'rails/engine/commands'
@@ -0,0 +1,18 @@
1
+ ---
2
+ cs:
3
+ activerecord:
4
+ models:
5
+ spree/relation:
6
+ one: Relation
7
+ other: Relations
8
+ spree/relation_type:
9
+ one: Relation Type
10
+ other: Typy vztahů
11
+ spree:
12
+ new_relation_type: Nový typ relace
13
+ related_product_discount: Sleva pro související zboží
14
+ applies_to: Platí pro
15
+ related_products: Související zboží
16
+ add_related_product: Nové související zboží
17
+ name_or_sku_short: Název nebo SKU
18
+ no_relation_types: 'Musíte nastavit typy vztahů, než budete moci použít tuto funkci.'