solidus_related_products 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,96 @@
1
+ RSpec.describe Spree::Admin::RelationsController, type: :controller do
2
+ stub_authorization!
3
+
4
+ let(:user) { create(:user) }
5
+ let!(:product) { create(:product) }
6
+ let!(:other1) { create(:product) }
7
+
8
+ let!(:relation_type) { create(:relation_type) }
9
+ let!(:relation) do
10
+ create(
11
+ :relation,
12
+ relatable: product,
13
+ related_to: other1,
14
+ relation_type: relation_type,
15
+ position: 0
16
+ )
17
+ end
18
+
19
+ before { stub_authentication! }
20
+ after { Spree::Admin::ProductsController.clear_overrides! }
21
+
22
+ context '.model_class' do
23
+ it 'responds to model_class as Spree::Relation' do
24
+ expect(controller.send(:model_class)).to eq Spree::Relation
25
+ end
26
+ end
27
+
28
+ describe 'with JS' do
29
+ sign_in_as_admin!
30
+
31
+ let(:valid_params) do
32
+ {
33
+ format: :js,
34
+ product_id: product.id,
35
+ relation: {
36
+ related_to_id: other1.id,
37
+ relation_type_id: relation_type.id
38
+ }
39
+ }
40
+ end
41
+
42
+ context '#create' do
43
+ it 'is not routable' do
44
+ spree_post :create, valid_params
45
+ expect(response.status).to be(200)
46
+ end
47
+
48
+ it 'returns success with valid params' do
49
+ expect {
50
+ spree_post :create, valid_params
51
+ }.to change(Spree::Relation, :count).by(1)
52
+ end
53
+
54
+ it 'raises error with invalid params' do
55
+ expect {
56
+ spree_post :create, format: :js
57
+ }.to raise_error
58
+ end
59
+ end
60
+
61
+ context '#update' do
62
+ it 'redirects to product/related url' do
63
+ spree_put :update, product_id: product.id, id: 1, relation: { discount_amount: 2.0 }
64
+ expect(response).to redirect_to(spree.admin_product_path(relation.relatable) + '/related')
65
+ end
66
+ end
67
+
68
+ context '#destroy' do
69
+ it 'records successfully' do
70
+ expect {
71
+ spree_delete :destroy, id: 1, product_id: product.id, format: :js
72
+ }.to change(Spree::Relation, :count).by(-1)
73
+ end
74
+ end
75
+
76
+ context '#update_positions' do
77
+ it 'returns the correct position of the related products' do
78
+ other2 = create(:product)
79
+ relation2 = create(
80
+ :relation, relatable: product, related_to: other2, relation_type: relation_type, position: 1
81
+ )
82
+
83
+ expect {
84
+ params = {
85
+ product_id: product.id,
86
+ id: relation.id,
87
+ positions: { relation.id => '1', relation2.id => '0' },
88
+ format: :js
89
+ }
90
+ spree_post :update_positions, params
91
+ relation.reload
92
+ }.to change(relation, :position).from(0).to(1)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,98 @@
1
+ RSpec.describe Spree::Api::RelationsController, type: :controller do
2
+ stub_authorization!
3
+ render_views
4
+
5
+ let(:user) { create(:user) }
6
+ let!(:product) { create(:product) }
7
+ let!(:other1) { create(:product) }
8
+
9
+ let!(:relation_type) { create(:relation_type) }
10
+ let!(:relation) do
11
+ create(
12
+ :relation,
13
+ relatable: product,
14
+ related_to: other1,
15
+ relation_type: relation_type,
16
+ position: 0
17
+ )
18
+ end
19
+
20
+ before { stub_authentication! }
21
+ after { Spree::Admin::ProductsController.clear_overrides! }
22
+
23
+ context 'model_class' do
24
+ it 'responds to model_class as Spree::Relation' do
25
+ expect(controller.send(:model_class)).to eq Spree::Relation
26
+ end
27
+ end
28
+
29
+ describe 'with JSON' do
30
+ sign_in_as_admin!
31
+
32
+ let(:valid_params) do
33
+ {
34
+ format: :json,
35
+ product_id: product.id,
36
+ relation: {
37
+ related_to_id: other1.id,
38
+ relation_type_id: relation_type.id
39
+ },
40
+ token: user.spree_api_key
41
+ }
42
+ end
43
+
44
+ context '#create' do
45
+ it 'creates the relation' do
46
+ spree_post :create, valid_params
47
+ expect(response.status).to eq(201)
48
+ end
49
+
50
+ it 'responds 422 error with invalid params' do
51
+ spree_post :create, format: :json, product_id: product.id, token: user.spree_api_key
52
+ expect(response.status).to eq(422)
53
+ end
54
+ end
55
+
56
+ context '#update' do
57
+ it 'succesfully updates the relation ' do
58
+ params = {
59
+ format: :json,
60
+ product_id: product.id,
61
+ id: relation.id,
62
+ relation: { discount_amount: 2.0 }
63
+ }
64
+ expect {
65
+ spree_put :update, params
66
+ }.to change { relation.reload.discount_amount.to_s }.from('0.0').to('2.0')
67
+ end
68
+ end
69
+
70
+ context '#destroy with' do
71
+ it 'records successfully' do
72
+ expect {
73
+ spree_delete :destroy, id: 1, product_id: product.id, format: :json, token: user.spree_api_key
74
+ }.to change(Spree::Relation, :count).by(-1)
75
+ end
76
+ end
77
+
78
+ context '#update_positions' do
79
+ it 'returns the correct position of the related products' do
80
+ other2 = create(:product)
81
+ relation2 = create(
82
+ :relation, relatable: product, related_to: other2, relation_type: relation_type, position: 1
83
+ )
84
+
85
+ expect {
86
+ params = {
87
+ product_id: product.id,
88
+ id: relation.id,
89
+ positions: { relation.id => '1', relation2.id => '0' },
90
+ format: :json
91
+ }
92
+ spree_post :update_positions, params
93
+ relation.reload
94
+ }.to change(relation, :position).from(0).to(1)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,7 @@
1
+ FactoryGirl.define do
2
+ factory :relation, class: Spree::Relation do
3
+ association :relatable, factory: :product
4
+ association :related_to, factory: :product
5
+ relation_type 'Spree::Product'
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ FactoryGirl.define do
2
+ factory :relation_type, class: Spree::RelationType do
3
+ name { generate(:random_string) }
4
+ applies_to 'Spree::Product'
5
+ end
6
+ end
@@ -0,0 +1,86 @@
1
+ RSpec.feature 'Admin Product Relation', :js do
2
+ stub_authorization!
3
+
4
+ given!(:product) { create(:product) }
5
+ given!(:other) { create(:product) }
6
+
7
+ given!(:relation_type) { create(:relation_type, name: 'Gears') }
8
+
9
+ background do
10
+ visit spree.edit_admin_product_path(product)
11
+ click_link 'Related Products'
12
+ end
13
+
14
+ scenario 'create relation' do
15
+ expect(page).to have_text 'ADD RELATED PRODUCT'
16
+ expect(page).to have_text product.name
17
+
18
+ within('#add-line-item') do
19
+ select2_search other.name, from: 'Name or SKU'
20
+ select2_search relation_type.name, from: 'Type'
21
+ fill_in 'add_discount', with: '0.8'
22
+ click_link 'Add'
23
+ end
24
+
25
+ wait_for_ajax
26
+
27
+ within_row(1) do
28
+ expect(page).to have_field('relation_discount_amount', with: '0.8')
29
+ expect(column_text(2)).to eq other.name
30
+ expect(column_text(3)).to eq relation_type.name
31
+ end
32
+ end
33
+
34
+ context 'with relations' do
35
+ given!(:relation) do
36
+ create(
37
+ :relation,
38
+ relatable: product,
39
+ related_to: other,
40
+ relation_type: relation_type,
41
+ discount_amount: 0.5
42
+ )
43
+ end
44
+
45
+ background do
46
+ visit spree.edit_admin_product_path(product)
47
+ click_link 'Related Products'
48
+ end
49
+
50
+ scenario 'ensure content exist' do
51
+ expect(page).to have_text 'ADD RELATED PRODUCT'
52
+ expect(page).to have_text product.name
53
+ expect(page).to have_text other.name
54
+
55
+ within_row(1) do
56
+ expect(page).to have_field('relation_discount_amount', with: '0.5')
57
+ expect(column_text(2)).to eq other.name
58
+ expect(column_text(3)).to eq relation_type.name
59
+ end
60
+ end
61
+
62
+ scenario 'update discount' do
63
+ within_row(1) do
64
+ fill_in 'relation_discount_amount', with: '0.9'
65
+ click_on 'Update'
66
+ end
67
+ wait_for_ajax
68
+ within_row(1) do
69
+ expect(page).to have_field('relation_discount_amount', with: '0.9')
70
+ end
71
+ end
72
+
73
+ context 'delete' do
74
+ scenario 'can remove records' do
75
+ expect(page).to have_text other.name
76
+ within_row(1) do
77
+ expect(column_text(2)).to eq other.name
78
+ click_icon :trash
79
+ end
80
+ page.driver.browser.switch_to.alert.accept unless Capybara.javascript_driver == :poltergeist
81
+ wait_for_ajax
82
+ expect(page).not_to have_text other.name
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,97 @@
1
+ RSpec.feature 'Admin Relation Types', :js do
2
+ stub_authorization!
3
+
4
+ background do
5
+ visit spree.admin_relation_types_path
6
+ end
7
+
8
+ scenario 'when no relation types exists' do
9
+ expect(page).to have_text 'NO RELATION TYPES FOUND, ADD ONE!'
10
+ end
11
+
12
+ context 'create' do
13
+ scenario 'can create a new relation type' do
14
+ click_link 'New Relation Type'
15
+ expect(current_path).to eq spree.new_admin_relation_type_path
16
+
17
+ fill_in 'Name', with: 'Gears'
18
+ fill_in 'Applies To', with: 'Spree:Products'
19
+
20
+ click_button 'Create'
21
+
22
+ expect(page).to have_text 'successfully created!'
23
+ expect(current_path).to eq spree.admin_relation_types_path
24
+ end
25
+
26
+ scenario 'shows validation errors with blank :name' do
27
+ click_link 'New Relation Type'
28
+ expect(current_path).to eq spree.new_admin_relation_type_path
29
+
30
+ fill_in 'Name', with: ''
31
+ click_button 'Create'
32
+
33
+ expect(page).to have_text 'Name can\'t be blank'
34
+ end
35
+
36
+ scenario 'shows validation errors with blank :applies_to' do
37
+ click_link 'New Relation Type'
38
+ expect(current_path).to eq spree.new_admin_relation_type_path
39
+
40
+ fill_in 'Name', with: 'Gears'
41
+ fill_in 'Applies To', with: ''
42
+ click_button 'Create'
43
+
44
+ expect(page).to have_text 'Applies to can\'t be blank'
45
+ end
46
+ end
47
+
48
+ context 'with records' do
49
+ background do
50
+ %w(Gears Equipments).each do |name|
51
+ create(:relation_type, name: name)
52
+ end
53
+ visit spree.admin_relation_types_path
54
+ end
55
+
56
+ context 'show' do
57
+ scenario 'displays existing relation types' do
58
+ within_row(1) do
59
+ expect(column_text(1)).to eq 'Gears'
60
+ expect(column_text(2)).to eq 'Spree::Product'
61
+ expect(column_text(3)).to eq ''
62
+ end
63
+ end
64
+ end
65
+
66
+ context 'edit' do
67
+ background do
68
+ within_row(1) { click_icon :edit }
69
+ expect(current_path).to eq spree.edit_admin_relation_type_path(1)
70
+ end
71
+
72
+ scenario 'can update an existing relation type' do
73
+ fill_in 'Name', with: 'Gadgets'
74
+ click_button 'Update'
75
+ expect(page).to have_text 'successfully updated!'
76
+ expect(page).to have_text 'Gadgets'
77
+ end
78
+
79
+ scenario 'shows validation errors with blank :name' do
80
+ fill_in 'Name', with: ''
81
+ click_button 'Update'
82
+ expect(page).to have_text 'Name can\'t be blank'
83
+ end
84
+ end
85
+
86
+ context 'delete' do
87
+ scenario 'can remove records' do
88
+ within_row(1) do
89
+ expect(column_text(1)).to eq 'Gears'
90
+ click_icon :trash
91
+ end
92
+ page.driver.browser.switch_to.alert.accept unless Capybara.javascript_driver == :poltergeist
93
+ expect(page).to have_text 'successfully removed!'
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,48 @@
1
+ RSpec.describe Spree::Calculator::RelatedProductDiscount, type: :model do
2
+ subject { described_class.new }
3
+
4
+ context '.description' do
5
+ it 'outputs relation product discount' do
6
+ expect(subject.description).to eq Spree.t(:related_product_discount)
7
+ end
8
+ end
9
+
10
+ describe '.compute(object)' do
11
+ it 'returns nil with empty Array' do
12
+ expect(subject.compute([])).to be_nil
13
+ end
14
+
15
+ it 'returns nil unless order is eligible' do
16
+ empty_order = double('Spree::Order')
17
+ allow(empty_order).to receive(:line_items).and_return([])
18
+ expect(subject.compute(empty_order)).to be_nil
19
+ end
20
+
21
+ context 'with order' do
22
+ before do
23
+ @order = double('Spree::Order')
24
+ product = build_stubbed(:product)
25
+ variant = double('Spree::Variant', product: product)
26
+ price = double('Spree::Price', variant: variant, amount: 5.00)
27
+ line_item = double('Spree::LineItem', variant: variant, order: @order, quantity: 1, price: 4.99)
28
+
29
+ allow(variant).to receive(:default_price).and_return(price)
30
+ allow(@order).to receive(:line_items).and_return([line_item])
31
+
32
+ related_product = create(:product)
33
+ relation_type = create(:relation_type)
34
+
35
+ create(:relation, relatable: product, related_to: related_product, relation_type: relation_type, discount_amount: 1.0)
36
+ end
37
+
38
+ it 'returns total count of Array' do
39
+ objects = Array.new { @order }
40
+ expect(subject.compute(objects)).to be_nil
41
+ end
42
+
43
+ it 'returns total count' do
44
+ expect(subject.compute(@order)).to be_zero
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,129 @@
1
+ RSpec.describe Spree::Product, type: :model do
2
+ context 'class' do
3
+ describe '.relation_types' do
4
+ it 'returns all the RelationTypes in use for this Product' do
5
+ relation_type = create(:relation_type)
6
+ expect(described_class.relation_types).to include(relation_type)
7
+ end
8
+ end
9
+ end
10
+
11
+ context 'relations' do
12
+ it { is_expected.to have_many(:relations) }
13
+ end
14
+
15
+ context 'instance' do
16
+ let(:other1) { create(:product) }
17
+ let(:other2) { create(:product) }
18
+
19
+ before do
20
+ @product = create(:product)
21
+ @relation_type = create(:relation_type, name: 'Related Products')
22
+ end
23
+
24
+ describe '.relations' do
25
+ it 'has many relations' do
26
+ relation1 = create(:relation, relatable: @product, related_to: other1, relation_type: @relation_type)
27
+ relation2 = create(:relation, relatable: @product, related_to: other2, relation_type: @relation_type)
28
+
29
+ @product.reload
30
+ expect(@product.relations).to include(relation1)
31
+ expect(@product.relations).to include(relation2)
32
+ end
33
+
34
+ it 'has many relations for different RelationTypes' do
35
+ other_relation_type = Spree::RelationType.new(name: 'Recommended Products')
36
+
37
+ relation1 = create(:relation, relatable: @product, related_to: other1, relation_type: @relation_type)
38
+ relation2 = create(:relation, relatable: @product, related_to: other1, relation_type: other_relation_type)
39
+
40
+ @product.reload
41
+ expect(@product.relations).to include(relation1)
42
+ expect(@product.relations).to include(relation2)
43
+ end
44
+ end
45
+
46
+ describe 'RelationType finders' do
47
+ before do
48
+ @relation = create(:relation, relatable: @product, related_to: other1, relation_type: @relation_type)
49
+ @product.reload
50
+ end
51
+
52
+ it 'returns the relevant relations' do
53
+ expect(@product.related_products).to include(other1)
54
+ end
55
+
56
+ it 'recognizes the method with has_related_products?(method)' do
57
+ expect(@product.has_related_products?('related_products')).to be_truthy
58
+ end
59
+
60
+ it 'does not recognize non-existent methods with has_related_products?(method)' do
61
+ expect(@product.has_related_products?('unrelated_products')).not_to be_truthy
62
+ end
63
+
64
+ it 'is the pluralised form of the RelationType name' do
65
+ @relation_type.update_attributes(name: 'Related Product')
66
+ expect(@product.related_products).to include(other1)
67
+ end
68
+
69
+ it 'does not return relations for another RelationType' do
70
+ other_relation_type = Spree::RelationType.new(name: 'Recommended Products')
71
+
72
+ create(:relation, relatable: @product, related_to: other1, relation_type: @relation_type)
73
+ create(:relation, relatable: @product, related_to: other2, relation_type: other_relation_type)
74
+
75
+ @product.reload
76
+ expect(@product.related_products).to include(other1)
77
+ expect(@product.related_products).not_to include(other2)
78
+ end
79
+
80
+ it 'does not return Products that are deleted' do
81
+ other1.update_attributes(deleted_at: Time.now)
82
+ expect(@product.related_products).to be_blank
83
+ end
84
+
85
+ it 'does not return Products that are not yet available' do
86
+ other1.update_attributes(available_on: Time.now + 1.hour)
87
+ expect(@product.related_products).to be_blank
88
+ end
89
+
90
+ it 'does not return Products where available_on are blank' do
91
+ other1.update_attributes(available_on: nil)
92
+ expect(@product.related_products).to be_blank
93
+ end
94
+
95
+ it 'returns all results when .relation_filter is nil' do
96
+ expect(described_class).to receive(:relation_filter).and_return(nil)
97
+ other1.update_attributes(available_on: Time.now + 1.hour)
98
+ expect(@product.related_products).to include(other1)
99
+ end
100
+
101
+ context 'with an enhanced Product.relation_filter' do
102
+ it 'restricts the filter' do
103
+ relation_filter = described_class.relation_filter
104
+ expect(described_class).to receive(:relation_filter).at_least(:once).and_return(relation_filter.includes(:master).where('spree_variants.cost_price > 20'))
105
+
106
+ other1.master.update_attributes(cost_price: 10)
107
+ other2.master.update_attributes(cost_price: 30)
108
+
109
+ create(:relation, relatable: @product, related_to: other2, relation_type: @relation_type)
110
+ results = @product.related_products
111
+ expect(results).not_to include(other1)
112
+ expect(results).to include(other2)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ context 'instance when relation_types table is missing' do
119
+ it 'method missing should not throw ActiveRecord::StatementInvalid when the spree_relation_types table is missing' do
120
+ described_class.connection.rename_table('spree_relation_types', 'missing_relation_types')
121
+ begin
122
+ product = described_class.new
123
+ expect { product.foo }.to raise_error(NameError)
124
+ ensure
125
+ described_class.connection.rename_table('missing_relation_types', 'spree_relation_types')
126
+ end
127
+ end
128
+ end
129
+ end