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
data/bin/setup
ADDED
@@ -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
|
data/config/routes.rb
ADDED
@@ -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,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
|