solidus_importer 0.1.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 (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