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,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