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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.hound.yml +25 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +15 -0
- data/CONTRIBUTING.md +81 -0
- data/Gemfile +6 -0
- data/Guardfile +10 -0
- data/LICENSE.md +26 -0
- data/README.md +80 -0
- data/Rakefile +15 -0
- data/app/assets/javascripts/spree/backend/solidus_related_products.js +1 -0
- data/app/assets/javascripts/spree/frontend/solidus_related_products.js +1 -0
- data/app/assets/stylesheets/spree/backend/solidus_related_products.css +3 -0
- data/app/assets/stylesheets/spree/frontend/solidus_related_products.css +3 -0
- data/app/controllers/spree/admin/products_controller_decorator.rb +10 -0
- data/app/controllers/spree/admin/relation_types_controller.rb +6 -0
- data/app/controllers/spree/admin/relations_controller.rb +80 -0
- data/app/controllers/spree/api/relations_controller.rb +78 -0
- data/app/models/spree/calculator/related_product_discount.rb +50 -0
- data/app/models/spree/product_decorator.rb +106 -0
- data/app/models/spree/relation.rb +7 -0
- data/app/models/spree/relation_type.rb +6 -0
- data/app/overrides/add_product_relation_admin_sub_menu_tab.rb +6 -0
- data/app/overrides/add_related_product_admin_tabs.rb +6 -0
- data/app/views/spree/admin/products/_related_products.html.erb +3 -0
- data/app/views/spree/admin/products/_related_products_table.html.erb +42 -0
- data/app/views/spree/admin/products/related.html.erb +70 -0
- data/app/views/spree/admin/relation_types/_form.html.erb +19 -0
- data/app/views/spree/admin/relation_types/edit.html.erb +14 -0
- data/app/views/spree/admin/relation_types/index.html.erb +44 -0
- data/app/views/spree/admin/relation_types/new.html.erb +14 -0
- data/app/views/spree/admin/relations/create.js.erb +5 -0
- data/app/views/spree/admin/relations/destroy.js.erb +1 -0
- data/app/views/spree/api/relations/show.v1.rabl +2 -0
- data/bin/rails +7 -0
- data/config/locales/cs.yml +18 -0
- data/config/locales/de.yml +18 -0
- data/config/locales/en.yml +18 -0
- data/config/locales/es.yml +18 -0
- data/config/locales/fr.yml +18 -0
- data/config/locales/it.yml +18 -0
- data/config/locales/nl.yml +18 -0
- data/config/locales/pl.yml +18 -0
- data/config/locales/pt-BR.yml +18 -0
- data/config/locales/ru.yml +18 -0
- data/config/locales/sv.yml +18 -0
- data/config/routes.rb +24 -0
- data/db/migrate/20100308090631_create_relation_types.rb +14 -0
- data/db/migrate/20100308092101_create_relations.rb +14 -0
- data/db/migrate/20100324123835_add_discount_to_relation.rb +9 -0
- data/db/migrate/20111129044813_prefixing_tables_with_spree.rb +6 -0
- data/db/migrate/20120208144454_update_relation_types.rb +9 -0
- data/db/migrate/20120623014337_update_relations.rb +11 -0
- data/db/migrate/20130727004612_add_position_to_spree_relations.rb +5 -0
- data/lib/generators/solidus_related_products/install/install_generator.rb +20 -0
- data/lib/solidus_related_products.rb +7 -0
- data/lib/solidus_related_products/engine.rb +24 -0
- data/lib/solidus_related_products/version.rb +18 -0
- data/solidus_related_products.gemspec +45 -0
- data/spec/controllers/spree/admin/products_controller_decorator_spec.rb +20 -0
- data/spec/controllers/spree/admin/relations_controller_spec.rb +96 -0
- data/spec/controllers/spree/api/relations_controller_spec.rb +98 -0
- data/spec/factories/relation_factory.rb +7 -0
- data/spec/factories/relation_type_factory.rb +6 -0
- data/spec/features/spree/admin/product_relation_spec.rb +86 -0
- data/spec/features/spree/admin/relation_types_spec.rb +97 -0
- data/spec/models/spree/calculator/related_product_discount_spec.rb +48 -0
- data/spec/models/spree/product_spec.rb +129 -0
- data/spec/models/spree/relation_spec.rb +13 -0
- data/spec/models/spree/relation_type_spec.rb +18 -0
- data/spec/spec_helper.rb +48 -0
- data/spec/support/capybara.rb +20 -0
- data/spec/support/database_cleaner.rb +23 -0
- data/spec/support/factory_girl.rb +7 -0
- data/spec/support/spree.rb +18 -0
- 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,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 @@
|
|
1
|
+
$('#products-table-wrapper').html('<%= escape_javascript(render "spree/admin/products/related_products_table", product: @product) %>');
|
data/bin/rails
ADDED
@@ -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.'
|