solidus_importer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +35 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +17 -0
  5. data/.gitignore +20 -0
  6. data/.hound.yml +8 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +2 -0
  9. data/Gemfile +33 -0
  10. data/LICENSE +26 -0
  11. data/README.md +170 -0
  12. data/Rakefile +6 -0
  13. data/app/assets/javascripts/spree/backend/solidus_importer.js +2 -0
  14. data/app/assets/javascripts/spree/frontend/solidus_importer.js +2 -0
  15. data/app/assets/stylesheets/spree/backend/solidus_importer.css +16 -0
  16. data/app/assets/stylesheets/spree/frontend/solidus_importer.css +4 -0
  17. data/app/controllers/spree/admin/solidus_importer/import_rows_controller.rb +24 -0
  18. data/app/controllers/spree/admin/solidus_importer/imports_controller.rb +44 -0
  19. data/app/jobs/solidus_importer/import_job.rb +30 -0
  20. data/app/models/solidus_importer/import.rb +48 -0
  21. data/app/models/solidus_importer/row.rb +26 -0
  22. data/app/views/spree/admin/solidus_importer/import_rows/show.html.erb +46 -0
  23. data/app/views/spree/admin/solidus_importer/imports/_form.html.erb +14 -0
  24. data/app/views/spree/admin/solidus_importer/imports/index.html.erb +80 -0
  25. data/app/views/spree/admin/solidus_importer/imports/new.html.erb +13 -0
  26. data/app/views/spree/admin/solidus_importer/imports/show.html.erb +87 -0
  27. data/bin/console +17 -0
  28. data/bin/rails +7 -0
  29. data/bin/rails-engine +13 -0
  30. data/bin/rails-sandbox +16 -0
  31. data/bin/rake +7 -0
  32. data/bin/sandbox +84 -0
  33. data/bin/setup +8 -0
  34. data/config/initializers/spree.rb +11 -0
  35. data/config/locales/en.yml +28 -0
  36. data/config/routes.rb +11 -0
  37. data/db/migrate/20191216101011_create_solidus_importer_imports.rb +19 -0
  38. data/db/migrate/20191216101012_create_solidus_importer_rows.rb +14 -0
  39. data/examples/importers/custom_importer.rb +20 -0
  40. data/examples/processors/notify_on_failure.rb +22 -0
  41. data/lib/generators/solidus_importer/install/install_generator.rb +22 -0
  42. data/lib/solidus_importer.rb +25 -0
  43. data/lib/solidus_importer/base_importer.rb +26 -0
  44. data/lib/solidus_importer/configuration.rb +39 -0
  45. data/lib/solidus_importer/engine.rb +23 -0
  46. data/lib/solidus_importer/exception.rb +5 -0
  47. data/lib/solidus_importer/factories.rb +4 -0
  48. data/lib/solidus_importer/process_import.rb +88 -0
  49. data/lib/solidus_importer/process_row.rb +46 -0
  50. data/lib/solidus_importer/processors/address.rb +47 -0
  51. data/lib/solidus_importer/processors/base.rb +15 -0
  52. data/lib/solidus_importer/processors/customer.rb +41 -0
  53. data/lib/solidus_importer/processors/log.rb +15 -0
  54. data/lib/solidus_importer/processors/option_types.rb +43 -0
  55. data/lib/solidus_importer/processors/option_values.rb +49 -0
  56. data/lib/solidus_importer/processors/order.rb +40 -0
  57. data/lib/solidus_importer/processors/product.rb +51 -0
  58. data/lib/solidus_importer/processors/product_images.rb +30 -0
  59. data/lib/solidus_importer/processors/taxon.rb +52 -0
  60. data/lib/solidus_importer/processors/variant.rb +51 -0
  61. data/lib/solidus_importer/processors/variant_images.rb +30 -0
  62. data/lib/solidus_importer/version.rb +5 -0
  63. data/solidus_importer.gemspec +36 -0
  64. data/spec/factories/solidus_importer_imports.rb +27 -0
  65. data/spec/factories/solidus_importer_rows.rb +82 -0
  66. data/spec/features/admin/solidus_importer/import_rows_spec.rb +30 -0
  67. data/spec/features/admin/solidus_importer/imports_spec.rb +121 -0
  68. data/spec/features/solidus_importer/import_spec.rb +138 -0
  69. data/spec/features/solidus_importer/processors_spec.rb +53 -0
  70. data/spec/fixtures/solidus_importer/apparel.csv +23 -0
  71. data/spec/fixtures/solidus_importer/customers.csv +3 -0
  72. data/spec/fixtures/solidus_importer/home-and-garden.csv +22 -0
  73. data/spec/fixtures/solidus_importer/invalid_headers.csv +3 -0
  74. data/spec/fixtures/solidus_importer/invalid_product.csv +2 -0
  75. data/spec/fixtures/solidus_importer/jewelery.csv +55 -0
  76. data/spec/fixtures/solidus_importer/orders.csv +5 -0
  77. data/spec/fixtures/solidus_importer/products.csv +8 -0
  78. data/spec/fixtures/solidus_importer/thinking-cat.jpg +0 -0
  79. data/spec/jobs/solidus_importer/import_job_spec.rb +54 -0
  80. data/spec/lib/solidus_importer/base_importer_spec.rb +23 -0
  81. data/spec/lib/solidus_importer/process_import_spec.rb +74 -0
  82. data/spec/lib/solidus_importer/process_row_spec.rb +24 -0
  83. data/spec/lib/solidus_importer/processors/address_spec.rb +18 -0
  84. data/spec/lib/solidus_importer/processors/base_spec.rb +9 -0
  85. data/spec/lib/solidus_importer/processors/customer_spec.rb +65 -0
  86. data/spec/lib/solidus_importer/processors/log_spec.rb +18 -0
  87. data/spec/lib/solidus_importer/processors/option_types_spec.rb +38 -0
  88. data/spec/lib/solidus_importer/processors/option_values_spec.rb +43 -0
  89. data/spec/lib/solidus_importer/processors/order_spec.rb +56 -0
  90. data/spec/lib/solidus_importer/processors/product_images_spec.rb +42 -0
  91. data/spec/lib/solidus_importer/processors/product_spec.rb +66 -0
  92. data/spec/lib/solidus_importer/processors/taxon_spec.rb +33 -0
  93. data/spec/lib/solidus_importer/processors/variant_images_spec.rb +44 -0
  94. data/spec/lib/solidus_importer/processors/variant_spec.rb +86 -0
  95. data/spec/lib/solidus_importer_spec.rb +14 -0
  96. data/spec/models/solidus_importer/import_spec.rb +60 -0
  97. data/spec/models/solidus_importer/row_spec.rb +18 -0
  98. data/spec/spec_helper.rb +27 -0
  99. data/spec/support/solidus_importer_helpers.rb +13 -0
  100. metadata +227 -0
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusImporter::Processors::OptionValues do
6
+ describe '#call' do
7
+ subject(:described_method) { described_class.call(context) }
8
+
9
+ let(:context) { { data: data, variant: variant } }
10
+ let(:variant) { create :base_variant }
11
+ let(:product) { variant.product }
12
+ let(:color) { create :option_type, presentation: 'Color' }
13
+ let(:size) { create :option_type, presentation: 'Size' }
14
+ let(:data) do
15
+ {
16
+ 'Option1 Name' => 'Size',
17
+ 'Option1 Value' => 'L',
18
+ 'Option2 Name' => 'Color',
19
+ 'Option2 Value' => 'Black'
20
+ }
21
+ end
22
+
23
+ before { product.option_types << size << color }
24
+
25
+ context 'when "Option(1,2,3) Value" are present' do
26
+ it 'create option values for variant in row' do
27
+ expect { described_method }.to change(variant.option_values, :count).from(0).to(2)
28
+ expect(variant.option_values.first.presentation).to eq 'L'
29
+ expect(variant.option_values.first.name).to eq 'L'
30
+ expect(variant.option_values.last.presentation).to eq 'Black'
31
+ expect(variant.option_values.last.name).to eq 'Black'
32
+ end
33
+
34
+ it 'creates "Black" option value for related "Color" option type' do
35
+ described_method
36
+ black = variant.option_values.find_by(presentation: 'Black')
37
+ l = variant.option_values.find_by(presentation: 'L')
38
+ expect(black.option_type).to eq color
39
+ expect(l.option_type).to eq size
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusImporter::Processors::Order do
6
+ describe '#call' do
7
+ subject(:described_method) { described_class.call(context) }
8
+
9
+ let(:context) { {} }
10
+
11
+ context 'without order number in row data' do
12
+ let(:context) do
13
+ { data: 'Some data' }
14
+ end
15
+
16
+ it 'raises an exception' do
17
+ expect { described_method }.to raise_error(SolidusImporter::Exception, 'Missing required key: "Name"')
18
+ end
19
+ end
20
+
21
+ context 'with an order row with a file entity' do
22
+ let(:context) do
23
+ { data: data }
24
+ end
25
+ let(:data) { build(:solidus_importer_row_order, :with_import).data }
26
+ let(:result) { context.merge(order: Spree::Order.last) }
27
+
28
+ before { allow(Spree::Store).to receive(:default).and_return(build_stubbed(:store)) }
29
+
30
+ it 'creates a new order' do
31
+ expect { described_method }.to change { Spree::Order.count }.by(1)
32
+ expect(described_method).to eq(result)
33
+ end
34
+
35
+ context 'with an existing valid order' do
36
+ let!(:order) { create(:order, number: data['Name'], email: data['Email']) }
37
+
38
+ it 'updates the order' do
39
+ expect { described_method }.not_to(change{ Spree::Order.count })
40
+ expect(described_method).to eq(result)
41
+ expect(order.reload.email).to eq('an_email@example.com')
42
+ end
43
+ end
44
+
45
+ context 'with an existing invalid order' do
46
+ let!(:order) { create(:order, number: data['Name'], email: data['Email']) }
47
+
48
+ before { order.update_column(:state, 'an invalid state') }
49
+
50
+ it 'raises an exception' do
51
+ expect { described_method }.to raise_error(ActiveRecord::RecordInvalid, /State is invalid/)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusImporter::Processors::ProductImages do
6
+ describe '#call' do
7
+ subject(:described_method) { described_class.call(context) }
8
+
9
+ let(:context) { {} }
10
+
11
+ context 'without "Image Src" attribute in row data' do
12
+ let(:context) do
13
+ { data: { 'Some attr' => 'some value' }, product: build_stubbed(:product) }
14
+ end
15
+
16
+ it 'returns nothing' do
17
+ expect(described_method).to be_nil
18
+ end
19
+ end
20
+
21
+ context 'with a product and a invalid image in row data' do
22
+ let(:context) do
23
+ { data: { 'Image Src' => 'some missing image' }, product: build_stubbed(:product) }
24
+ end
25
+
26
+ it 'raises an exception' do
27
+ expect { described_method }.to raise_error(Errno::ENOENT, /No such file or directory/)
28
+ end
29
+ end
30
+
31
+ context 'with a product and a valid image in row data' do
32
+ let(:image_path) { solidus_importer_fixture_path('thinking-cat.jpg') }
33
+ let(:context) do
34
+ { data: { 'Image Src' => image_path }, product: build_stubbed(:product) }
35
+ end
36
+
37
+ it 'adds images to the product' do
38
+ expect { described_method }.to change(context[:product].images, :any?).from(false).to(true)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusImporter::Processors::Product do
6
+ describe '#call' do
7
+ subject(:described_method) { described_class.call(context) }
8
+
9
+ let(:context) { {} }
10
+
11
+ context 'without product slug in row data' do
12
+ let(:context) do
13
+ { data: 'Some data' }
14
+ end
15
+
16
+ it 'raises an exception' do
17
+ expect { described_method }.to raise_error(SolidusImporter::Exception, 'Missing required key: "Handle"')
18
+ end
19
+ end
20
+
21
+ context 'with a product row with a file entity' do
22
+ let(:context) do
23
+ { data: data }
24
+ end
25
+ let(:data) { build(:solidus_importer_row_product, :with_import).data }
26
+ let(:product) { Spree::Product.last }
27
+ let(:result) { context.merge(product: product) }
28
+ let!(:shipping_category) { create(:shipping_category) }
29
+
30
+ it 'creates a new product' do
31
+ expect { described_method }.to change { Spree::Product.count }.by(1)
32
+ expect(described_method).to eq(result)
33
+ expect(product).not_to be_available
34
+ end
35
+
36
+ context 'when "Published" is "true"' do
37
+ before { data.merge! 'Published' => 'true' }
38
+
39
+ it 'creates an available product' do
40
+ described_method
41
+ expect(product).to be_available
42
+ end
43
+ end
44
+
45
+ context 'when "Published" is false' do
46
+ before { data.merge! 'Published' => 'false' }
47
+
48
+ it 'creates an available product' do
49
+ described_method
50
+ expect(product).not_to be_available
51
+ end
52
+ end
53
+
54
+ context 'with an existing valid product' do
55
+ let(:result) { { data: data, product: product } }
56
+ let!(:product) { create(:product, slug: data['Handle']) }
57
+
58
+ it 'updates the product' do
59
+ expect { described_method }.not_to(change { Spree::Product.count })
60
+ expect(described_method).to eq(result)
61
+ expect(product.reload.name).to eq('A product name')
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusImporter::Processors::Taxon do
6
+ describe '#call' do
7
+ subject(:described_method) { described_class.call(context) }
8
+
9
+ let!(:shipping_category) { create :shipping_category }
10
+ let(:context) { { data: data, product: product } }
11
+ let(:product) { create :product }
12
+ let(:data) { build(:solidus_importer_row_product, :with_import, :with_type, :with_tags).data }
13
+ let(:tags_taxonomy) { Spree::Taxonomy.find_by(name: 'Tags') }
14
+ let(:type_taxonomy) { Spree::Taxonomy.find_by(name: 'Type') }
15
+
16
+ it 'create a taxon for each tag, related to the given taxonomy' do
17
+ expect { described_method }.to change(product.taxons, :count).by(4)
18
+
19
+ %w[Type1 Tag1 Tag2 Tag3].each do |taxon_name|
20
+ expect(product.taxons.map(&:name)).to include(taxon_name)
21
+ end
22
+
23
+ expect(Spree::Taxon.find_by(name: 'Tag1').taxonomy).to eq tags_taxonomy
24
+ expect(Spree::Taxon.find_by(name: 'Type1').taxonomy).to eq type_taxonomy
25
+ end
26
+
27
+ it 'creates "Type" and "Tags" taxonomies' do
28
+ described_method
29
+ expect(type_taxonomy).to be_present
30
+ expect(tags_taxonomy).to be_present
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusImporter::Processors::VariantImages do
6
+ describe '#call' do
7
+ subject(:described_method) { described_class.call(context) }
8
+
9
+ let(:context) { {} }
10
+
11
+ context 'without "Variant Image" attribute in row data' do
12
+ let(:context) do
13
+ { data: { 'Some attr' => 'some value' }, product: build_stubbed(:product) }
14
+ end
15
+
16
+ it 'returns nothing' do
17
+ expect(described_method).to be_nil
18
+ end
19
+ end
20
+
21
+ context 'with a variant and a invalid image in row data' do
22
+ let(:context) do
23
+ { data: { 'Variant Image' => 'some missing image' }, variant: build_stubbed(:variant) }
24
+ end
25
+
26
+ it 'raises an exception' do
27
+ expect { described_method }.to raise_error(Errno::ENOENT, /No such file or directory/)
28
+ end
29
+ end
30
+
31
+ context 'with a variant and a valid image in row data' do
32
+ let(:context) do
33
+ {
34
+ data: { 'Variant Image' => solidus_importer_fixture_path('thinking-cat.jpg') },
35
+ variant: build_stubbed(:variant)
36
+ }
37
+ end
38
+
39
+ it 'adds images to the variant' do
40
+ expect { described_method }.to change(context[:variant].images, :any?).from(false).to(true)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusImporter::Processors::Variant do
6
+ describe '#call' do
7
+ subject(:described_method) { described_class.call(context) }
8
+
9
+ let(:context) { {} }
10
+
11
+ context 'without a target product' do
12
+ let(:context) do
13
+ { data: { 'Variant SKU' => 'some SKU' }, product: nil }
14
+ end
15
+
16
+ it 'raises an exception' do
17
+ expect { described_method }.to raise_error(ArgumentError, 'missing :product in context')
18
+ end
19
+ end
20
+
21
+ context 'with a variant row, a product and a source file' do
22
+ let(:context) do
23
+ { data: data, product: product }
24
+ end
25
+ let(:data) { build(:solidus_importer_row_variant, :with_import).data }
26
+ let!(:product) { create(:product, slug: data['Handle']) }
27
+
28
+ before do
29
+ allow(Spree::ShippingCategory).to receive(:last).and_return(build_stubbed(:shipping_category))
30
+ end
31
+
32
+ context 'when "Option1 Value" is "Some value"' do
33
+ let(:result) { context.merge(variant: product.variants.first) }
34
+
35
+ before { data.merge! 'Option1 Value' => 'Some value' }
36
+
37
+ it 'creates a new variant' do
38
+ expect { described_method }.to change { Spree::Variant.count }.by(1)
39
+ expect(described_method).to eq(result)
40
+ expect(product.variants.first.weight).to eq 20.0
41
+ expect(product.variants.first.price).to eq 60.5
42
+ end
43
+ end
44
+
45
+ context 'when "Option1 Value" is "Default Title"' do
46
+ let(:result) { context.merge(variant: product.master) }
47
+
48
+ before { data.merge! 'Option1 Value' => 'Default Title' }
49
+
50
+ it 'updates master variant' do
51
+ expect { described_method }.not_to(change { Spree::Variant.count })
52
+ expect(described_method).to eq(result)
53
+ expect(product.master.weight).to eq 20.0
54
+ expect(product.master.price).to eq 60.5
55
+ end
56
+ end
57
+
58
+ context 'when "Option1 Value" is blank' do
59
+ let(:result) { context.merge(variant: product.master) }
60
+
61
+ before { data.merge! 'Option1 Value' => nil }
62
+
63
+ it 'updates master variant' do
64
+ expect { described_method }.not_to(change { Spree::Variant.count })
65
+ expect(described_method).to eq(result)
66
+ expect(product.master.weight).to eq 20.0
67
+ expect(product.master.price).to eq 60.5
68
+ end
69
+ end
70
+
71
+ context 'with an existing valid variant' do
72
+ let(:result) { context.merge(variant: variant) }
73
+ let!(:variant) { create(:variant, sku: data['Variant SKU'], price: 0, weight: 10.0) }
74
+
75
+ before { data.merge! 'Option1 Value' => 'Some value' }
76
+
77
+ it 'updates the variant' do
78
+ expect { described_method }.not_to(change { Spree::Variant.count })
79
+ expect(described_method).to eq(result)
80
+ expect(variant.reload.weight.to_f).to eq(data['Variant Weight'])
81
+ expect(variant.reload.price).to eq(data['Variant Price'])
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusImporter do
6
+ describe '.import!' do
7
+ subject(:described_method) { described_class.import!('target-import.csv', type: 'customers') }
8
+
9
+ it 'invokes ProcessImport#import_from_file' do
10
+ expect(SolidusImporter::ProcessImport).to receive(:import_from_file).with('target-import.csv', :customers)
11
+ subject
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusImporter::Import do
6
+ subject(:described_instance) { described_class.new }
7
+
8
+ it { expect(described_instance).not_to be_valid }
9
+
10
+ context 'with an import file and type' do
11
+ before do
12
+ described_instance.import_type = :customers
13
+ described_instance.import_file = solidus_importer_fixture_path('customers.csv')
14
+ end
15
+
16
+ it { expect(described_instance).to be_valid }
17
+ end
18
+
19
+ describe '#import_file=' do
20
+ subject(:described_method) { described_instance.import_file = some_file }
21
+
22
+ let(:some_file) { nil }
23
+
24
+ it { expect { described_method }.to raise_error(SolidusImporter::Exception) }
25
+
26
+ context 'with a file' do
27
+ let(:some_file) { __FILE__ }
28
+
29
+ it { expect { described_method }.to change { described_instance.file.present? }.from(false).to(true) }
30
+ end
31
+ end
32
+
33
+ describe '#finished?' do
34
+ subject(:described_method) { described_instance.finished? }
35
+
36
+ let(:rows) { OpenStruct.new }
37
+ let(:finished_rows) { [] }
38
+ let(:size) { 0 }
39
+
40
+ before do
41
+ allow(rows).to receive_messages(failed_or_completed: finished_rows, size: size)
42
+ allow(described_instance).to receive_messages(rows: rows)
43
+ end
44
+
45
+ it { is_expected.to be_truthy }
46
+
47
+ context 'with some rows' do
48
+ let(:finished_rows) { [1, 2] }
49
+ let(:size) { 3 }
50
+
51
+ it { is_expected.to be_falsey }
52
+
53
+ context 'with the size equals to the number of failed or completed rows' do
54
+ let(:size) { finished_rows.size }
55
+
56
+ it { is_expected.to be_truthy }
57
+ end
58
+ end
59
+ end
60
+ end