solidus_importer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +35 -0
- data/.gem_release.yml +5 -0
- data/.github/stale.yml +17 -0
- data/.gitignore +20 -0
- data/.hound.yml +8 -0
- data/.rspec +2 -0
- data/.rubocop.yml +2 -0
- data/Gemfile +33 -0
- data/LICENSE +26 -0
- data/README.md +170 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/spree/backend/solidus_importer.js +2 -0
- data/app/assets/javascripts/spree/frontend/solidus_importer.js +2 -0
- data/app/assets/stylesheets/spree/backend/solidus_importer.css +16 -0
- data/app/assets/stylesheets/spree/frontend/solidus_importer.css +4 -0
- data/app/controllers/spree/admin/solidus_importer/import_rows_controller.rb +24 -0
- data/app/controllers/spree/admin/solidus_importer/imports_controller.rb +44 -0
- data/app/jobs/solidus_importer/import_job.rb +30 -0
- data/app/models/solidus_importer/import.rb +48 -0
- data/app/models/solidus_importer/row.rb +26 -0
- data/app/views/spree/admin/solidus_importer/import_rows/show.html.erb +46 -0
- data/app/views/spree/admin/solidus_importer/imports/_form.html.erb +14 -0
- data/app/views/spree/admin/solidus_importer/imports/index.html.erb +80 -0
- data/app/views/spree/admin/solidus_importer/imports/new.html.erb +13 -0
- data/app/views/spree/admin/solidus_importer/imports/show.html.erb +87 -0
- data/bin/console +17 -0
- data/bin/rails +7 -0
- data/bin/rails-engine +13 -0
- data/bin/rails-sandbox +16 -0
- data/bin/rake +7 -0
- data/bin/sandbox +84 -0
- data/bin/setup +8 -0
- data/config/initializers/spree.rb +11 -0
- data/config/locales/en.yml +28 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20191216101011_create_solidus_importer_imports.rb +19 -0
- data/db/migrate/20191216101012_create_solidus_importer_rows.rb +14 -0
- data/examples/importers/custom_importer.rb +20 -0
- data/examples/processors/notify_on_failure.rb +22 -0
- data/lib/generators/solidus_importer/install/install_generator.rb +22 -0
- data/lib/solidus_importer.rb +25 -0
- data/lib/solidus_importer/base_importer.rb +26 -0
- data/lib/solidus_importer/configuration.rb +39 -0
- data/lib/solidus_importer/engine.rb +23 -0
- data/lib/solidus_importer/exception.rb +5 -0
- data/lib/solidus_importer/factories.rb +4 -0
- data/lib/solidus_importer/process_import.rb +88 -0
- data/lib/solidus_importer/process_row.rb +46 -0
- data/lib/solidus_importer/processors/address.rb +47 -0
- data/lib/solidus_importer/processors/base.rb +15 -0
- data/lib/solidus_importer/processors/customer.rb +41 -0
- data/lib/solidus_importer/processors/log.rb +15 -0
- data/lib/solidus_importer/processors/option_types.rb +43 -0
- data/lib/solidus_importer/processors/option_values.rb +49 -0
- data/lib/solidus_importer/processors/order.rb +40 -0
- data/lib/solidus_importer/processors/product.rb +51 -0
- data/lib/solidus_importer/processors/product_images.rb +30 -0
- data/lib/solidus_importer/processors/taxon.rb +52 -0
- data/lib/solidus_importer/processors/variant.rb +51 -0
- data/lib/solidus_importer/processors/variant_images.rb +30 -0
- data/lib/solidus_importer/version.rb +5 -0
- data/solidus_importer.gemspec +36 -0
- data/spec/factories/solidus_importer_imports.rb +27 -0
- data/spec/factories/solidus_importer_rows.rb +82 -0
- data/spec/features/admin/solidus_importer/import_rows_spec.rb +30 -0
- data/spec/features/admin/solidus_importer/imports_spec.rb +121 -0
- data/spec/features/solidus_importer/import_spec.rb +138 -0
- data/spec/features/solidus_importer/processors_spec.rb +53 -0
- data/spec/fixtures/solidus_importer/apparel.csv +23 -0
- data/spec/fixtures/solidus_importer/customers.csv +3 -0
- data/spec/fixtures/solidus_importer/home-and-garden.csv +22 -0
- data/spec/fixtures/solidus_importer/invalid_headers.csv +3 -0
- data/spec/fixtures/solidus_importer/invalid_product.csv +2 -0
- data/spec/fixtures/solidus_importer/jewelery.csv +55 -0
- data/spec/fixtures/solidus_importer/orders.csv +5 -0
- data/spec/fixtures/solidus_importer/products.csv +8 -0
- data/spec/fixtures/solidus_importer/thinking-cat.jpg +0 -0
- data/spec/jobs/solidus_importer/import_job_spec.rb +54 -0
- data/spec/lib/solidus_importer/base_importer_spec.rb +23 -0
- data/spec/lib/solidus_importer/process_import_spec.rb +74 -0
- data/spec/lib/solidus_importer/process_row_spec.rb +24 -0
- data/spec/lib/solidus_importer/processors/address_spec.rb +18 -0
- data/spec/lib/solidus_importer/processors/base_spec.rb +9 -0
- data/spec/lib/solidus_importer/processors/customer_spec.rb +65 -0
- data/spec/lib/solidus_importer/processors/log_spec.rb +18 -0
- data/spec/lib/solidus_importer/processors/option_types_spec.rb +38 -0
- data/spec/lib/solidus_importer/processors/option_values_spec.rb +43 -0
- data/spec/lib/solidus_importer/processors/order_spec.rb +56 -0
- data/spec/lib/solidus_importer/processors/product_images_spec.rb +42 -0
- data/spec/lib/solidus_importer/processors/product_spec.rb +66 -0
- data/spec/lib/solidus_importer/processors/taxon_spec.rb +33 -0
- data/spec/lib/solidus_importer/processors/variant_images_spec.rb +44 -0
- data/spec/lib/solidus_importer/processors/variant_spec.rb +86 -0
- data/spec/lib/solidus_importer_spec.rb +14 -0
- data/spec/models/solidus_importer/import_spec.rb +60 -0
- data/spec/models/solidus_importer/row_spec.rb +18 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/solidus_importer_helpers.rb +13 -0
- metadata +227 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusImporter
|
4
|
+
module Processors
|
5
|
+
class Customer < Base
|
6
|
+
attr_accessor :address
|
7
|
+
|
8
|
+
def call(context)
|
9
|
+
@data = context.fetch(:data)
|
10
|
+
check_data
|
11
|
+
|
12
|
+
self.address = context[:address]
|
13
|
+
|
14
|
+
context.merge!(user: process_user)
|
15
|
+
end
|
16
|
+
|
17
|
+
def options
|
18
|
+
@options ||= {
|
19
|
+
password_method: ->(_user) { SecureRandom.alphanumeric(8) }
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def check_data
|
26
|
+
raise SolidusImporter::Exception, 'Missing required key: "Email"' if @data['Email'].blank?
|
27
|
+
end
|
28
|
+
|
29
|
+
def prepare_user
|
30
|
+
Spree::User.find_or_initialize_by(email: @data['Email']) do |u|
|
31
|
+
u.password = options[:password_method].call(u)
|
32
|
+
u.bill_address = address if address.present?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def process_user
|
37
|
+
prepare_user.tap(&:save!)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusImporter
|
4
|
+
module Processors
|
5
|
+
class Log < Base
|
6
|
+
def call(context)
|
7
|
+
Spree::LogEntry.create!(
|
8
|
+
source_id: context[:row_id],
|
9
|
+
source_type: 'SolidusImporter::Row',
|
10
|
+
details: context.except(:importer, :data).to_json
|
11
|
+
)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusImporter
|
4
|
+
module Processors
|
5
|
+
class OptionTypes < Base
|
6
|
+
def call(context)
|
7
|
+
@data = context.fetch(:data)
|
8
|
+
return unless option_types?
|
9
|
+
|
10
|
+
product = context.fetch(:product)
|
11
|
+
context.merge!(option_types: process_option_types(product))
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def process_option_types(product)
|
17
|
+
option_type_names.each_with_index.map do |name, i|
|
18
|
+
option_type = product.option_types.find_or_initialize_by(
|
19
|
+
name: name
|
20
|
+
)
|
21
|
+
option_type.presentation = name
|
22
|
+
option_type.name = name.downcase
|
23
|
+
option_type.position = i + 1
|
24
|
+
option_type.save
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def option_type_names
|
29
|
+
@option_type_names ||= @data.values_at(
|
30
|
+
'Option1 Name',
|
31
|
+
'Option2 Name',
|
32
|
+
'Option3 Name'
|
33
|
+
).compact
|
34
|
+
end
|
35
|
+
|
36
|
+
def option_types?
|
37
|
+
# NOTE: according to https://help.shopify.com/en/manual/products/import-export
|
38
|
+
# when `Option Name1` is equal to 'Title`, means that product has no variants.
|
39
|
+
(@data['Option1 Name'] != 'Title') && option_type_names.any?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusImporter
|
4
|
+
module Processors
|
5
|
+
class OptionValues < Base
|
6
|
+
attr_accessor :option_types, :variant
|
7
|
+
|
8
|
+
def call(context)
|
9
|
+
@data = context.fetch(:data)
|
10
|
+
return unless option_values?
|
11
|
+
|
12
|
+
self.variant = context.fetch(:variant)
|
13
|
+
process_option_values
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def process_option_values
|
19
|
+
option_value_names.each_with_index do |name, i|
|
20
|
+
option_value = Spree::OptionValue.find_or_initialize_by(
|
21
|
+
option_type: option_type(i),
|
22
|
+
name: name
|
23
|
+
)
|
24
|
+
option_value.presentation = name
|
25
|
+
variant.option_values << option_value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def option_type(index)
|
30
|
+
variant.product.option_types.find { |ot| ot.position == index + 1 }
|
31
|
+
end
|
32
|
+
|
33
|
+
def option_value_names
|
34
|
+
@option_value_names ||= @data.values_at(
|
35
|
+
'Option1 Value',
|
36
|
+
'Option2 Value',
|
37
|
+
'Option3 Value'
|
38
|
+
).compact
|
39
|
+
end
|
40
|
+
|
41
|
+
def option_values?
|
42
|
+
# NOTE: according to https://help.shopify.com/en/manual/products/import-export
|
43
|
+
# when `Option Value1` is equal to 'Default Title`, means that product has no
|
44
|
+
# variants.
|
45
|
+
(@data['Option1 Value'] != 'Default Title') && option_value_names.any?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusImporter
|
4
|
+
module Processors
|
5
|
+
class Order < Base
|
6
|
+
def call(context)
|
7
|
+
@data = context.fetch(:data)
|
8
|
+
check_data
|
9
|
+
context.merge!(order: process_order)
|
10
|
+
end
|
11
|
+
|
12
|
+
def options
|
13
|
+
@options ||= {
|
14
|
+
store: Spree::Store.default
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def check_data
|
21
|
+
raise SolidusImporter::Exception, 'Missing required key: "Name"' if @data['Name'].blank?
|
22
|
+
end
|
23
|
+
|
24
|
+
def prepare_order
|
25
|
+
Spree::Order.find_or_initialize_by(number: @data['Name']) do |order|
|
26
|
+
order.store = options[:store]
|
27
|
+
end.tap do |order|
|
28
|
+
# Apply the row attributes
|
29
|
+
order.currency = @data['Currency'] unless @data['Currency'].nil?
|
30
|
+
order.email = @data['Email'] unless @data['Email'].nil?
|
31
|
+
order.special_instructions = @data['Note'] unless @data['Note'].nil?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_order
|
36
|
+
prepare_order.tap(&:save!)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusImporter
|
4
|
+
module Processors
|
5
|
+
class Product < Base
|
6
|
+
def call(context)
|
7
|
+
@data = context.fetch(:data)
|
8
|
+
check_data
|
9
|
+
context.merge!(product: process_product)
|
10
|
+
end
|
11
|
+
|
12
|
+
def options
|
13
|
+
@options ||= {
|
14
|
+
available_on: Date.current.yesterday,
|
15
|
+
not_available: nil,
|
16
|
+
price: 0,
|
17
|
+
shipping_category: Spree::ShippingCategory.find_by(name: 'Default') || Spree::ShippingCategory.first
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def check_data
|
24
|
+
raise SolidusImporter::Exception, 'Missing required key: "Handle"' if @data['Handle'].blank?
|
25
|
+
end
|
26
|
+
|
27
|
+
def prepare_product
|
28
|
+
Spree::Product.find_or_initialize_by(slug: @data['Handle'])
|
29
|
+
end
|
30
|
+
|
31
|
+
def process_product
|
32
|
+
prepare_product.tap do |product|
|
33
|
+
product.slug = @data['Handle']
|
34
|
+
product.price = options[:price]
|
35
|
+
product.available_on = available? ? options[:available_on] : options[:not_available]
|
36
|
+
product.shipping_category = options[:shipping_category]
|
37
|
+
|
38
|
+
# Apply the row attributes
|
39
|
+
product.name = @data['Title'] unless @data['Title'].nil?
|
40
|
+
|
41
|
+
# Save the product
|
42
|
+
product.save!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def available?
|
47
|
+
@data['Published'] == 'true'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusImporter
|
4
|
+
module Processors
|
5
|
+
class ProductImages < Base
|
6
|
+
def call(context)
|
7
|
+
@data = context.fetch(:data)
|
8
|
+
return unless product_image?
|
9
|
+
|
10
|
+
product = context.fetch(:product)
|
11
|
+
process_images(product)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def prepare_image
|
17
|
+
attachment = URI.open(@data['Image Src'])
|
18
|
+
Spree::Image.new(attachment: attachment, alt: @data['Alt Text'])
|
19
|
+
end
|
20
|
+
|
21
|
+
def process_images(product)
|
22
|
+
product.images << prepare_image
|
23
|
+
end
|
24
|
+
|
25
|
+
def product_image?
|
26
|
+
@product_image ||= @data['Image Src'].present?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusImporter
|
4
|
+
module Processors
|
5
|
+
class Taxon < Base
|
6
|
+
attr_accessor :product, :taxonomy
|
7
|
+
|
8
|
+
def call(context)
|
9
|
+
@data = context.fetch(:data)
|
10
|
+
|
11
|
+
self.product = context.fetch(:product)
|
12
|
+
|
13
|
+
process_taxons_type
|
14
|
+
process_taxons_tags
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def options
|
20
|
+
@options ||= {
|
21
|
+
type_taxonomy: Spree::Taxonomy.find_or_create_by(name: 'Type'),
|
22
|
+
tags_taxonomy: Spree::Taxonomy.find_or_create_by(name: 'Tags')
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def process_taxons_type
|
27
|
+
product.taxons << prepare_taxon(type, options[:type_taxonomy])
|
28
|
+
end
|
29
|
+
|
30
|
+
def process_taxons_tags
|
31
|
+
tags.map do |tag|
|
32
|
+
product.taxons << prepare_taxon(tag, options[:tags_taxonomy])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def prepare_taxon(name, taxonomy)
|
37
|
+
Spree::Taxon.find_or_initialize_by(
|
38
|
+
name: name,
|
39
|
+
taxonomy_id: taxonomy.id
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def tags
|
44
|
+
@data['Tags'].split(',').map(&:strip)
|
45
|
+
end
|
46
|
+
|
47
|
+
def type
|
48
|
+
@data['Type']
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusImporter
|
4
|
+
module Processors
|
5
|
+
class Variant < Base
|
6
|
+
attr_accessor :product
|
7
|
+
|
8
|
+
def call(context)
|
9
|
+
@data = context.fetch(:data)
|
10
|
+
self.product = context.fetch(:product) || raise(ArgumentError, 'missing :product in context')
|
11
|
+
|
12
|
+
context.merge!(variant: process_variant)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def prepare_variant
|
18
|
+
return product.master if master_variant?
|
19
|
+
|
20
|
+
@prepare_variant ||= Spree::Variant.find_or_initialize_by(sku: sku) do |variant|
|
21
|
+
variant.product = product
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def process_variant
|
26
|
+
prepare_variant.tap do |variant|
|
27
|
+
# Apply the row attributes
|
28
|
+
variant.weight = @data['Variant Weight'] unless @data['Variant Weight'].nil?
|
29
|
+
variant.price = @data['Variant Price'] if @data['Variant Price'].present?
|
30
|
+
|
31
|
+
# Save the product
|
32
|
+
variant.save!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def master_variant?
|
37
|
+
ov1 = @data['Option1 Value']
|
38
|
+
ov1.blank? || ov1 == 'Default Title'
|
39
|
+
end
|
40
|
+
|
41
|
+
def sku
|
42
|
+
@data['Variant SKU'] || generate_sku
|
43
|
+
end
|
44
|
+
|
45
|
+
def generate_sku
|
46
|
+
variant_part = @data['Option1 Value'].parameterize
|
47
|
+
"#{product.slug}-#{variant_part}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusImporter
|
4
|
+
module Processors
|
5
|
+
class VariantImages < Base
|
6
|
+
def call(context)
|
7
|
+
@data = context.fetch(:data)
|
8
|
+
return unless variant_image?
|
9
|
+
|
10
|
+
variant = context.fetch(:variant)
|
11
|
+
process_images(variant)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def prepare_image
|
17
|
+
attachment = URI.open(@data['Variant Image'])
|
18
|
+
Spree::Image.new(attachment: attachment)
|
19
|
+
end
|
20
|
+
|
21
|
+
def process_images(variant)
|
22
|
+
variant.images << prepare_image
|
23
|
+
end
|
24
|
+
|
25
|
+
def variant_image?
|
26
|
+
@variant_image ||= @data['Variant Image'].present?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/solidus_importer/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'solidus_importer'
|
7
|
+
spec.version = SolidusImporter::VERSION
|
8
|
+
spec.authors = ['Mattia Roccoberton']
|
9
|
+
spec.email = 'contact@solidus.io'
|
10
|
+
|
11
|
+
spec.summary = 'Solidus Importer extension'
|
12
|
+
spec.description = 'Solidus Importer extension to migrate data from other eCommerce systems'
|
13
|
+
spec.homepage = 'https://github.com/nebulab/solidus_importer#readme'
|
14
|
+
spec.license = 'BSD-3-Clause'
|
15
|
+
|
16
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
17
|
+
spec.metadata['source_code_uri'] = 'https://github.com/nebulab/solidus_importer#readme'
|
18
|
+
spec.metadata['changelog_uri'] = 'https://github.com/nebulab/solidus_importer/releases'
|
19
|
+
|
20
|
+
spec.required_ruby_version = Gem::Requirement.new('~> 2.5')
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
|
25
|
+
|
26
|
+
spec.files = files.grep_v(%r{^(test|spec|features)/})
|
27
|
+
spec.test_files = files.grep(%r{^(test|spec|features)/})
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.add_dependency 'solidus_core', ['>= 2.0.0', '< 3']
|
33
|
+
spec.add_dependency 'solidus_support', '~> 0.5'
|
34
|
+
|
35
|
+
spec.add_development_dependency 'solidus_dev_support'
|
36
|
+
end
|