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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ gem install bundler --conservative
7
+ bundle update
8
+ bin/rake clobber
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spree::Backend::Config.configure do |config|
4
+ config.menu_items << config.class::MenuItem.new(
5
+ :imports,
6
+ 'download',
7
+ condition: -> { can?(:admin, Spree::Product) },
8
+ label: :importer,
9
+ url: :admin_solidus_importer_imports_path
10
+ )
11
+ end
@@ -0,0 +1,28 @@
1
+ en:
2
+ spree:
3
+ solidus_importer:
4
+ created_at: Created at
5
+ data: Data
6
+ import: Import
7
+ imports: Imports
8
+ import_rows: Import rows
9
+ log_entry: Log information
10
+ messages: Messages
11
+ target_entity: Target entity
12
+ title: &solidus_importer_title
13
+ Importer
14
+ updated_at: Updated at
15
+ updated_at_from: 'Updated at: start date'
16
+ updated_at_to: 'Updated at: end date'
17
+ import_type: Import Type
18
+ import_types:
19
+ customers: Customers
20
+ orders: Orders
21
+ products: Products
22
+
23
+ admin:
24
+ tab:
25
+ importer: *solidus_importer_title
26
+ solidus_importer:
27
+ create: Import!
28
+ new: New import
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spree::Core::Engine.routes.draw do
4
+ namespace :admin do
5
+ namespace :solidus_importer do
6
+ resources :imports, only: [:create, :index, :new, :show] do
7
+ resources :import_rows, only: [:show]
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSolidusImporterImports < ActiveRecord::Migration[5.2]
4
+ def change
5
+ create_table :solidus_importer_imports do |t|
6
+ t.string :import_type
7
+ t.string :state, null: false, default: 'created', limit: 32
8
+ t.string :file, null: false, default: '', limit: 1024
9
+ t.text :messages
10
+
11
+ t.timestamps null: false
12
+ end
13
+
14
+ reversible do |dir|
15
+ dir.up { add_attachment :solidus_importer_imports, :file }
16
+ dir.down { remove_attachment :solidus_importer_imports, :file }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSolidusImporterRows < ActiveRecord::Migration[5.2]
4
+ def change
5
+ create_table :solidus_importer_rows do |t|
6
+ t.belongs_to :import
7
+ t.string :state, null: false, default: 'created', limit: 32
8
+ t.text :data
9
+ t.text :messages
10
+
11
+ t.timestamps null: false
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Importers
5
+ ##
6
+ # This importer sends an email when the import process is finished.
7
+ #
8
+ # Install: set the class as importer in solidus_importer configuration
9
+ class CustomImporter < ::SolidusImporter::BaseImporter
10
+ def after_import(context)
11
+ ActionMailer::Base.mail(
12
+ from: 'some_email',
13
+ to: 'some_email_2',
14
+ subject: 'Import finished',
15
+ body: "Ending context:\n#{context.to_json}"
16
+ ).deliver_now
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ ##
6
+ # This processor sends an email if the process of a single row is failing.
7
+ #
8
+ # Install: add the class to processors in solidus_importer configuration
9
+ class NotifyOnFailure < Base
10
+ def call(context)
11
+ return if context[:success]
12
+
13
+ ActionMailer::Base.mail(
14
+ from: 'some_email',
15
+ to: 'some_email_2',
16
+ subject: 'Row process error',
17
+ body: "Row context:\n#{context.to_json}"
18
+ ).deliver_now
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ class_option :auto_run_migrations, type: :boolean, default: false
7
+
8
+ def add_migrations
9
+ run 'bin/rails railties:install:migrations FROM=solidus_importer'
10
+ end
11
+
12
+ def run_migrations
13
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]')) # rubocop:disable Metrics/LineLength
14
+ if run_migrations
15
+ run 'bin/rails db:migrate'
16
+ else
17
+ puts 'Skipping bin/rails db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'solidus_core'
4
+ require 'solidus_support'
5
+
6
+ require 'solidus_importer/version'
7
+ require 'solidus_importer/exception'
8
+ require 'solidus_importer/base_importer'
9
+
10
+ require 'solidus_importer/processors/base'
11
+ processors = File.join(__dir__, 'solidus_importer/processors/*.rb')
12
+ Dir[processors].each { |file| require file }
13
+
14
+ require 'solidus_importer/configuration'
15
+ require 'solidus_importer/engine'
16
+ require 'solidus_importer/process_import'
17
+ require 'solidus_importer/process_row'
18
+
19
+ module SolidusImporter
20
+ class << self
21
+ def import!(import_path, type:)
22
+ ProcessImport.import_from_file(import_path, type.to_sym)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ class BaseImporter
5
+ def initialize(options)
6
+ @options = options
7
+ end
8
+
9
+ def processors
10
+ @options[:processors] || []
11
+ end
12
+
13
+ ##
14
+ # Defines a method called before the import process is started.
15
+ # Remember to always return a context with `success` key.
16
+ #
17
+ # - initial_context: context used process the rows, example: `{ success: true }`
18
+ def before_import(initial_context)
19
+ initial_context
20
+ end
21
+
22
+ ##
23
+ # Defines a method called after the import process is started
24
+ def after_import(_ending_context); end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ class Configuration < Spree::Preferences::Configuration
5
+ preference :solidus_importer, :hash, default: {
6
+ customers: {
7
+ importer: SolidusImporter::BaseImporter,
8
+ processors: [
9
+ SolidusImporter::Processors::Address,
10
+ SolidusImporter::Processors::Customer,
11
+ SolidusImporter::Processors::Log
12
+ ]
13
+ },
14
+ orders: {
15
+ importer: SolidusImporter::BaseImporter,
16
+ processors: [
17
+ SolidusImporter::Processors::Order,
18
+ SolidusImporter::Processors::Log
19
+ ]
20
+ },
21
+ products: {
22
+ importer: SolidusImporter::BaseImporter,
23
+ processors: [
24
+ SolidusImporter::Processors::Product,
25
+ SolidusImporter::Processors::Variant,
26
+ SolidusImporter::Processors::OptionTypes,
27
+ SolidusImporter::Processors::OptionValues,
28
+ SolidusImporter::Processors::ProductImages,
29
+ SolidusImporter::Processors::VariantImages,
30
+ SolidusImporter::Processors::Log
31
+ ]
32
+ }
33
+ }
34
+
35
+ def available_types
36
+ solidus_importer.keys
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spree/core'
4
+ require 'solidus_importer'
5
+
6
+ module SolidusImporter
7
+ class Engine < Rails::Engine
8
+ include SolidusSupport::EngineExtensions
9
+
10
+ isolate_namespace ::Spree
11
+
12
+ engine_name 'solidus_importer'
13
+
14
+ initializer 'solidus_importer.environment', before: :load_config_initializers do |_app|
15
+ SolidusImporter::Config = SolidusImporter::Configuration.new
16
+ end
17
+
18
+ # use rspec for tests
19
+ config.generators do |g|
20
+ g.test_framework :rspec
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ class Exception < StandardError; end
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ module SolidusImporter
6
+ ##
7
+ # This class parse the source file and create the rows (scan). Then it asks to
8
+ # Process Row to process each one.
9
+ class ProcessImport
10
+ attr_reader :importer
11
+
12
+ def initialize(import, importer_options: nil)
13
+ @import = import
14
+ options = importer_options || ::SolidusImporter::Config.solidus_importer[@import.import_type.to_sym]
15
+ @importer = options[:importer].new(options)
16
+ @import.importer = @importer
17
+ validate!
18
+ end
19
+
20
+ def process(force_scan: nil)
21
+ return @import unless @import.created_or_failed?
22
+
23
+ scan_required = force_scan.nil? ? @import.created? : force_scan
24
+ @import.update(state: :processing)
25
+ initial_context = scan_required ? scan : { success: true }
26
+ initial_context = @importer.before_import(initial_context)
27
+ unless @import.failed?
28
+ rows = process_rows(initial_context)
29
+ @import.update(state: :completed) if rows.zero?
30
+ end
31
+ @import
32
+ end
33
+
34
+ class << self
35
+ def import_from_file(import_path, import_type)
36
+ import = ::SolidusImporter::Import.new(import_type: import_type)
37
+ import.import_file = import_path
38
+ import.save!
39
+ new(import).process
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def scan
46
+ data = CSV.parse(
47
+ File.read(@import.file.path),
48
+ headers: true,
49
+ encoding: 'UTF-8',
50
+ header_converters: ->(h) { h.strip }
51
+ )
52
+ prepare_rows(data)
53
+ end
54
+
55
+ def check_data(data)
56
+ messages = []
57
+ headers = data.headers
58
+ messages << 'Invalid headers' if headers.blank? || !headers.exclude?(nil)
59
+ messages
60
+ end
61
+
62
+ def prepare_rows(data)
63
+ messages = check_data(data)
64
+ if messages.empty?
65
+ data.each do |row|
66
+ @import.rows << ::SolidusImporter::Row.new(data: row.to_h)
67
+ end
68
+ { success: true }
69
+ else
70
+ @import.update(state: :failed, messages: messages.join(', '))
71
+ { success: false, messages: messages.join(', ') }
72
+ end
73
+ end
74
+
75
+ def process_rows(initial_context)
76
+ rows = @import.rows.created_or_failed.order(id: :asc)
77
+ rows.each do |row|
78
+ ::SolidusImporter::ProcessRow.new(@importer, row).process(initial_context)
79
+ end
80
+ rows.size
81
+ end
82
+
83
+ def validate!
84
+ raise ::SolidusImporter::Exception, 'Valid import entity required' if !@import || !@import.valid?
85
+ raise ::SolidusImporter::Exception, "No importer found for #{@import.import_type} type" if !@importer
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ ##
5
+ # This class process a single row of an import running the configured
6
+ # processor in chain.
7
+ class ProcessRow
8
+ def initialize(importer, row)
9
+ @importer = importer
10
+ @row = row
11
+ validate!
12
+ end
13
+
14
+ def process(initial_context)
15
+ context = initial_context.dup.merge!(row_id: @row.id, importer: @importer, data: @row.data)
16
+ @importer.processors.each do |processor|
17
+ begin
18
+ processor.call(context)
19
+ rescue StandardError => e
20
+ context.merge!(success: false, messages: e.message) # rubocop:disable Performance/RedundantMerge
21
+ break
22
+ end
23
+ end
24
+ @row.update!(
25
+ state: context[:success] ? :completed : :failed,
26
+ messages: context[:messages]
27
+ )
28
+ check_import_finished(context)
29
+ context
30
+ end
31
+
32
+ private
33
+
34
+ def check_import_finished(context)
35
+ return unless @row.import.finished?
36
+
37
+ @importer.after_import(context)
38
+ @row.import.update!(state: (@row.import.rows.failed.any? ? :failed : :completed))
39
+ end
40
+
41
+ def validate!
42
+ raise SolidusImporter::Exception, 'No importer defined' if !@importer
43
+ raise SolidusImporter::Exception, 'Invalid row type' if !@row || !@row.is_a?(SolidusImporter::Row)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class Address < Base
6
+ def call(context)
7
+ @data = context.fetch(:data)
8
+
9
+ return unless address?
10
+
11
+ context.merge!(address: process_address)
12
+ end
13
+
14
+ private
15
+
16
+ def process_address
17
+ prepare_address.tap(&:save)
18
+ end
19
+
20
+ def prepare_address
21
+ Spree::Address.new(
22
+ firstname: @data['First Name'],
23
+ lastname: @data['Last Name'],
24
+ address1: @data['Address1'],
25
+ address2: @data['Address2'],
26
+ city: @data['City'],
27
+ zipcode: @data['Zip'],
28
+ phone: @data['Phone'],
29
+ country: extract_country(@data['Country Code']),
30
+ state: extract_state(@data['Province Code'])
31
+ )
32
+ end
33
+
34
+ def extract_country(iso)
35
+ Spree::Country.find_by(iso: iso)
36
+ end
37
+
38
+ def extract_state(iso)
39
+ Spree::State.find_by(abbr: iso) || Spree::State.find_by(name: iso)
40
+ end
41
+
42
+ def address?
43
+ @data['Address1'].present?
44
+ end
45
+ end
46
+ end
47
+ end