datashift 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +14 -98
- data/VERSION +1 -1
- data/datashift.gemspec +6 -4
- data/lib/applications/jruby/jexcel_file.rb +69 -52
- data/lib/datashift/method_detail.rb +3 -2
- data/lib/datashift/method_dictionary.rb +7 -2
- data/lib/datashift/method_mapper.rb +12 -3
- data/lib/datashift/model_mapper.rb +4 -4
- data/lib/generators/excel_generator.rb +17 -6
- data/lib/loaders/excel_loader.rb +3 -2
- data/lib/loaders/loader_base.rb +62 -27
- data/lib/loaders/paperclip/image_loader.rb +75 -0
- data/lib/loaders/spree/image_loader.rb +11 -42
- data/lib/loaders/spree/product_loader.rb +94 -52
- data/lib/thor/generate_excel.thor +5 -2
- data/lib/thor/spree/bootstrap_cleanup.thor +22 -16
- data/lib/thor/spree/products_images.thor +58 -45
- data/lib/thor/spree/reports.thor +66 -0
- data/spec/db/migrate/20110803201325_create_test_bed.rb +12 -1
- data/spec/excel_generator_spec.rb +39 -1
- data/spec/excel_loader_spec.rb +1 -0
- data/spec/fixtures/datashift_Spree_db.sqlite +0 -0
- data/spec/fixtures/datashift_test_models_db.sqlite +0 -0
- data/spec/fixtures/spree/SpreeMultiVariant.csv +4 -0
- data/spec/fixtures/spree/SpreeProductsSimple.csv +1 -1
- data/spec/fixtures/spree/SpreeProductsSimple.xls +0 -0
- data/spec/fixtures/test_model_defs.rb +4 -0
- data/spec/spree_images_loader_spec.rb +11 -29
- data/spec/spree_loader_spec.rb +60 -15
- data/tasks/db_tasks.rake +45 -0
- metadata +6 -4
- data/datashift-0.6.0.gem +0 -0
- data/datashift-0.6.1.gem +0 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
# Copyright:: (c) Autotelik Media Ltd 2012
|
2
|
+
# Author :: Tom Statter
|
3
|
+
# Date :: March 2012
|
4
|
+
# License:: MIT. Free, Open Source.
|
5
|
+
#
|
6
|
+
# Usage::
|
7
|
+
# bundle exec thor help datashift:spreeboot
|
8
|
+
# bundle exec thor datashift:spreeboot:cleanup
|
9
|
+
#
|
10
|
+
# Note, not DataShift, case sensitive, create namespace for command line : datashift
|
11
|
+
|
12
|
+
require 'excel_exporter'
|
13
|
+
|
14
|
+
module Datashift
|
15
|
+
|
16
|
+
class Reports < Thor
|
17
|
+
|
18
|
+
include DataShift::Logging
|
19
|
+
|
20
|
+
desc "missing_images", "Spree Products without an image"
|
21
|
+
|
22
|
+
def missing_images(report = nil)
|
23
|
+
|
24
|
+
require 'spree_helper'
|
25
|
+
require 'image_loader'
|
26
|
+
|
27
|
+
require File.expand_path('config/environment.rb')
|
28
|
+
|
29
|
+
klass = DataShift::SpreeHelper::get_spree_class('Product')
|
30
|
+
|
31
|
+
missing = klass.all.find_all {|p| p.images.size == 0 }
|
32
|
+
|
33
|
+
puts "There are #{missing.size} Products without an associated Image"
|
34
|
+
|
35
|
+
if(DataShift::Guards::jruby?)
|
36
|
+
fname = report ? report : "missing_images.xls"
|
37
|
+
DataShift::ExcelExporter.new( fname ).export( missing )
|
38
|
+
else
|
39
|
+
puts missing.collect(&:name).inspect
|
40
|
+
end
|
41
|
+
|
42
|
+
@drop_box = "/home/stattert/Dropbox/DaveWebsiteInfo/"
|
43
|
+
|
44
|
+
@image_list = %w{
|
45
|
+
010InafixTheArmourGodAllFolders
|
46
|
+
01Figuresurbanlandscapepaintings
|
47
|
+
01FinishedArtPrints
|
48
|
+
02Seascapespainting
|
49
|
+
03Landscapes
|
50
|
+
04Spain
|
51
|
+
|
52
|
+
07SignsJohnAllFolders
|
53
|
+
07_Mar
|
54
|
+
09Powerpointsermonaids
|
55
|
+
}
|
56
|
+
|
57
|
+
options = { :recursive => true }
|
58
|
+
|
59
|
+
images = @image_list.collect do |p|
|
60
|
+
DataShift::ImageLoading::get_files(File.join(@drop_box,p), options)
|
61
|
+
end
|
62
|
+
|
63
|
+
puts images.inspect
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -9,7 +9,14 @@ class CreateTestBed < ActiveRecord::Migration
|
|
9
9
|
|
10
10
|
def self.up
|
11
11
|
|
12
|
+
create_table :users do |t|
|
13
|
+
t.string :title
|
14
|
+
t.string :first_name
|
15
|
+
end
|
16
|
+
|
17
|
+
# belongs_to :user
|
12
18
|
# has many :milestones
|
19
|
+
#
|
13
20
|
create_table :projects do |t|
|
14
21
|
t.string :title
|
15
22
|
t.string :value_as_string
|
@@ -18,10 +25,13 @@ class CreateTestBed < ActiveRecord::Migration
|
|
18
25
|
t.datetime :value_as_datetime, :default => nil
|
19
26
|
t.integer :value_as_integer, :default => 0
|
20
27
|
t.decimal :value_as_double, :precision => 8, :scale => 2, :default => 0.0
|
28
|
+
t.references :user
|
21
29
|
t.timestamps
|
22
30
|
end
|
23
31
|
|
24
|
-
# belongs_to :project
|
32
|
+
# belongs_to :project
|
33
|
+
# @project => has_many :milestones
|
34
|
+
|
25
35
|
create_table :milestones do |t|
|
26
36
|
t.string :name
|
27
37
|
t.datetime :datetime, :default => nil
|
@@ -73,6 +83,7 @@ class CreateTestBed < ActiveRecord::Migration
|
|
73
83
|
end
|
74
84
|
|
75
85
|
def self.down
|
86
|
+
drop_table :users
|
76
87
|
drop_table :projects
|
77
88
|
drop_table :categories
|
78
89
|
drop_table :loader_releases
|
@@ -56,7 +56,7 @@ if(Guards::jruby?)
|
|
56
56
|
puts "Can manually check file @ #{expect}"
|
57
57
|
end
|
58
58
|
|
59
|
-
it "should
|
59
|
+
it "should include all associations in template .xls file from model" do
|
60
60
|
|
61
61
|
expect= result_file('project_plus_assoc_template_spec.xls')
|
62
62
|
|
@@ -67,7 +67,45 @@ if(Guards::jruby?)
|
|
67
67
|
File.exists?(expect).should be_true
|
68
68
|
|
69
69
|
end
|
70
|
+
|
71
|
+
|
72
|
+
it "should enable us to exclude cetain associations in template .xls file from model" do
|
73
|
+
|
74
|
+
expect= result_file('project_plus_some_assoc_template_spec.xls')
|
75
|
+
|
76
|
+
gen = ExcelGenerator.new(expect)
|
77
|
+
|
78
|
+
options = {:exclude => :milestones }
|
79
|
+
|
80
|
+
gen.generate_with_associations(Project, options)
|
81
|
+
|
82
|
+
File.exists?(expect).should be_true
|
83
|
+
|
84
|
+
excel = JExcelFile.new(expect)
|
85
|
+
|
86
|
+
excel.each_row {|r| puts r.inspect }
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
it "should enable us to autosize columns in the .xls file" do
|
92
|
+
|
93
|
+
expect= result_file('project_autosized_template_spec.xls')
|
94
|
+
|
95
|
+
gen = ExcelGenerator.new(expect)
|
96
|
+
|
97
|
+
options = {:autosize => true, :exclude => :milestones }
|
98
|
+
|
99
|
+
gen.generate_with_associations(Project, options)
|
70
100
|
|
101
|
+
File.exists?(expect).should be_true
|
102
|
+
|
103
|
+
excel = JExcelFile.new(expect)
|
104
|
+
|
105
|
+
excel.each_row {|r| puts r.inspect }
|
106
|
+
|
107
|
+
end
|
108
|
+
|
71
109
|
end
|
72
110
|
else
|
73
111
|
puts "WARNING: skipped excel_generator_spec : Requires JRUBY - JExcelFile requires JAVA"
|
data/spec/excel_loader_spec.rb
CHANGED
@@ -114,6 +114,7 @@ if(Guards::jruby?)
|
|
114
114
|
loader.perform_load( $DataShiftFixturePath + '/ProjectsMultiCategoriesHeaderLookup.xls')
|
115
115
|
|
116
116
|
loader.loaded_count.should == (Project.count - count)
|
117
|
+
loader.loaded_count.should > 3
|
117
118
|
|
118
119
|
{'004' => 4, '005' => 1, '006' => 0, '007' => 1 }.each do|title, expected|
|
119
120
|
project = Project.find_by_title(title)
|
Binary file
|
Binary file
|
@@ -0,0 +1,4 @@
|
|
1
|
+
"SKU","Name","Description","Available On"," Price","CostPrice","Option Types","Option Types","Variants","count_on_hand"
|
2
|
+
"MV_001","Demo Product for AR Loader","blah blah","2011-02-14",399.99,320.00,"mime_type:jpeg, PDF;print_type:colour",,"mime_type:PNG","12|6|7"
|
3
|
+
"MV_002","Demo Excel Load via Jruby","less blah","2011-05-14",100.00,30.00,"mime_type:jpeg;print_type:black_white",,"mime_type:PNG;print_type:black_white","5|4"
|
4
|
+
"MV_003","Demo third row in future","more blah blah","2012-07-01",50.34,23.34,"mime_type:jpeg;print_type:colour, sepia;size:large|mime_type:PNG","mime_type:PDF|print_type:black_white",,"12|4|7|12"
|
@@ -1,4 +1,4 @@
|
|
1
1
|
"SKU","Name","Description","Available On"," Price","CostPrice","count_on_hand","Option Types"
|
2
2
|
"SIMPLE_001","Simple Product for AR Loader","blah blah","2011-02-14",345.78,320.00,12,"mime_type"
|
3
3
|
"SIMPLE_002","Simple Excel Load via Jruby","less blah","2011-05-14",100.00,30.00,5,"mime_type"
|
4
|
-
"SIMPLE_003","Simple third row avail in future","more blah blah","
|
4
|
+
"SIMPLE_003","Simple third row avail in future","more blah blah","2112-07-01",50.34,23.34,23,"mime_type|print_type"
|
Binary file
|
@@ -35,7 +35,10 @@ describe 'SpreeImageLoading' do
|
|
35
35
|
@Product_klass.count.should == 0
|
36
36
|
|
37
37
|
MethodDictionary.clear
|
38
|
-
|
38
|
+
|
39
|
+
# For Spree important to get instance methods too as Product delegates
|
40
|
+
# many important attributes to Variant (master)
|
41
|
+
MethodDictionary.find_operators( @Product_klass, :instance_methods => true )
|
39
42
|
|
40
43
|
@product_loader = DataShift::SpreeHelper::ProductLoader.new
|
41
44
|
rescue => e
|
@@ -45,17 +48,13 @@ describe 'SpreeImageLoading' do
|
|
45
48
|
end
|
46
49
|
|
47
50
|
|
48
|
-
it "should
|
49
|
-
|
50
|
-
# In >= 1.1.0 Image moved to master Variant from Product
|
51
|
-
|
51
|
+
it "should create Image from path in Product loading column from CSV", :fail => true do
|
52
|
+
|
52
53
|
options = {:mandatory => ['sku', 'name', 'price']}
|
53
54
|
|
54
|
-
options[:force_inclusion] = ['sku', 'images'] if(SpreeHelper::version.to_f > 1 )
|
55
|
-
|
56
55
|
@product_loader.perform_load( SpecHelper::spree_fixture('SpreeProductsWithImages.csv'), options )
|
57
56
|
|
58
|
-
@Image_klass.all.each_with_index {|i, x| puts "
|
57
|
+
@Image_klass.all.each_with_index {|i, x| puts "SPEC CHECK IMAGE #{x}", i.inspect }
|
59
58
|
|
60
59
|
p = @Product_klass.find_by_name("Demo Product for AR Loader")
|
61
60
|
|
@@ -70,15 +69,11 @@ describe 'SpreeImageLoading' do
|
|
70
69
|
end
|
71
70
|
|
72
71
|
|
73
|
-
it "should
|
72
|
+
it "should create Image from path in Product loading column from Excel", :fail => true do
|
74
73
|
|
75
74
|
options = {:mandatory => ['sku', 'name', 'price']}
|
76
75
|
|
77
|
-
options[:force_inclusion] = ['sku', 'images'] if(SpreeHelper::version.to_f > 1 )
|
78
|
-
|
79
76
|
@product_loader.perform_load( SpecHelper::spree_fixture('SpreeProductsWithImages.xls'), options )
|
80
|
-
|
81
|
-
@Image_klass.all.each_with_index {|i, x| puts "RESULT IMAGE #{x}", i.inspect }
|
82
77
|
|
83
78
|
p = @klass.find_by_name("Demo Product for AR Loader")
|
84
79
|
|
@@ -91,7 +86,9 @@ describe 'SpreeImageLoading' do
|
|
91
86
|
|
92
87
|
end
|
93
88
|
|
94
|
-
it "should be able to assign Images to preloaded Products"
|
89
|
+
it "should be able to assign Images to preloaded Products" do
|
90
|
+
|
91
|
+
pending "Currently functionality supplied by a thor task images()"
|
95
92
|
|
96
93
|
MethodDictionary.find_operators( @Image_klass )
|
97
94
|
|
@@ -101,24 +98,9 @@ describe 'SpreeImageLoading' do
|
|
101
98
|
|
102
99
|
@Image_klass.all.size.should == 0
|
103
100
|
|
104
|
-
# force inclusion means add to operator list even if not present
|
105
|
-
options = { :verbose => true, :force_inclusion => ['sku', 'attachment'] } if(SpreeHelper::version.to_f > 1 )
|
106
|
-
|
107
101
|
loader = DataShift::SpreeHelper::ImageLoader.new(nil, options)
|
108
102
|
|
109
103
|
loader.perform_load( SpecHelper::spree_fixture('SpreeImages.xls'), options )
|
110
|
-
|
111
|
-
@Image_klass.all.each_with_index {|i, x| puts "RESULT IMAGE #{x}", i.inspect }
|
112
|
-
|
113
|
-
@Image_klass.count.should == 3
|
114
|
-
|
115
|
-
p = @klass.find_by_name("Demo Product for AR Loader")
|
116
|
-
|
117
|
-
p.name.should == "Demo Product for AR Loader"
|
118
|
-
|
119
|
-
p.images.should have_exactly(1).items
|
120
|
-
|
121
|
-
@Product_klass.all.each {|p| p.images.should have_exactly(1).items }
|
122
104
|
|
123
105
|
end
|
124
106
|
|
data/spec/spree_loader_spec.rb
CHANGED
@@ -30,8 +30,7 @@ describe 'SpreeLoader' do
|
|
30
30
|
before(:each) do
|
31
31
|
|
32
32
|
begin
|
33
|
-
|
34
|
-
|
33
|
+
|
35
34
|
before_each_spree
|
36
35
|
|
37
36
|
@Product_klass.count.should == 0
|
@@ -39,7 +38,10 @@ describe 'SpreeLoader' do
|
|
39
38
|
@Variant_klass.count.should == 0
|
40
39
|
|
41
40
|
MethodDictionary.clear
|
42
|
-
|
41
|
+
|
42
|
+
# For Spree important to get instance methods too as Product delegates
|
43
|
+
# many important attributes to Variant (master)
|
44
|
+
MethodDictionary.find_operators( @Product_klass, :instance_methods => true )
|
43
45
|
|
44
46
|
# want to test both lookup and dynamic creation - this Taxonomy should be found, rest created
|
45
47
|
root = @Taxonomy_klass.create( :name => 'Paintings' )
|
@@ -80,25 +82,24 @@ describe 'SpreeLoader' do
|
|
80
82
|
loader.loaded_count.should == @Zone_klass.count
|
81
83
|
end
|
82
84
|
|
85
|
+
it "should raise an error for missing file" do
|
86
|
+
lambda { test_basic_product('SpreeProductsSimple.txt') }.should raise_error BadFile
|
87
|
+
end
|
83
88
|
|
89
|
+
it "should raise an error for unsupported file types" do
|
90
|
+
lambda { test_basic_product('SpreeProductsDefaults.yml') }.should raise_error UnsupportedFileType
|
91
|
+
end
|
92
|
+
|
84
93
|
# Loader should perform identically regardless of source, whether csv, .xls etc
|
85
94
|
|
86
|
-
it "should load basic Products .xls via Spree loader", :
|
95
|
+
it "should load basic Products .xls via Spree loader", :opts => true do
|
87
96
|
test_basic_product('SpreeProductsSimple.xls')
|
88
97
|
end
|
89
98
|
|
90
|
-
it "should load basic Products from .csv via Spree loader", :csv => true
|
99
|
+
it "should load basic Products from .csv via Spree loader", :csv => true do
|
91
100
|
test_basic_product('SpreeProductsSimple.csv')
|
92
101
|
end
|
93
102
|
|
94
|
-
it "should raise an error for missing file" do
|
95
|
-
lambda { test_basic_product('SpreeProductsSimple.txt') }.should raise_error BadFile
|
96
|
-
end
|
97
|
-
|
98
|
-
it "should raise an error for unsupported file types" do
|
99
|
-
lambda { test_basic_product('SpreeProductsDefaults.yml') }.should raise_error UnsupportedFileType
|
100
|
-
end
|
101
|
-
|
102
103
|
def test_basic_product( source )
|
103
104
|
|
104
105
|
@product_loader.perform_load( SpecHelper::spree_fixture(source), :mandatory => ['sku', 'name', 'price'] )
|
@@ -224,13 +225,13 @@ describe 'SpreeLoader' do
|
|
224
225
|
p.option_types[0].name.should == "mime_type"
|
225
226
|
p.option_types[0].presentation.should == "Mime type"
|
226
227
|
|
227
|
-
@Variant_klass.all[1].sku.should == "
|
228
|
+
@Variant_klass.all[1].sku.should == "DEMO_001_1"
|
228
229
|
@Variant_klass.all[1].price.should == 399.99
|
229
230
|
|
230
231
|
# V1
|
231
232
|
v1 = p.variants[0]
|
232
233
|
|
233
|
-
v1.sku.should == "
|
234
|
+
v1.sku.should == "DEMO_001_1"
|
234
235
|
v1.price.should == 399.99
|
235
236
|
v1.count_on_hand.should == 12
|
236
237
|
|
@@ -258,7 +259,50 @@ describe 'SpreeLoader' do
|
|
258
259
|
|
259
260
|
@product_loader.failed_objects.size.should == 0
|
260
261
|
end
|
262
|
+
|
263
|
+
# Composite Variant Syntax is option_type_A_name:value;option_type_B_name:value
|
264
|
+
# which creates a SINGLE Variant with 2 option types
|
261
265
|
|
266
|
+
it "should create Variants with MULTIPLE option types from single column", :new => true do
|
267
|
+
@product_loader.perform_load( SpecHelper::spree_fixture('SpreeMultiVariant.csv'), :mandatory => ['sku', 'name', 'price'] )
|
268
|
+
|
269
|
+
# Product 1)
|
270
|
+
# 1 + 2) mime_type:jpeg,PDF;print_type:colour equivalent to (mime_type:jpeg;print_type:colour|mime_type:PDF;print_type:colour)
|
271
|
+
# 3) mime_type:PNG
|
272
|
+
#
|
273
|
+
# Product 2
|
274
|
+
# 4) mime_type:jpeg;print_type:black_white
|
275
|
+
# 5) mime_type:PNG;print_type:black_white
|
276
|
+
#
|
277
|
+
# Product 3
|
278
|
+
# 6 +7) mime_type:jpeg;print_type:colour,sepia;size:large
|
279
|
+
# 8) mime_type:jpeg;print_type:colour
|
280
|
+
# 9) mime_type:PNG
|
281
|
+
# 9 + 10) mime_type:PDF|print_type:black_white
|
282
|
+
|
283
|
+
prod_count = 3
|
284
|
+
var_count = 10
|
285
|
+
|
286
|
+
# plus 3 MASTER VARIANTS
|
287
|
+
@Product_klass.count.should == prod_count
|
288
|
+
@Variant_klass.count.should == prod_count + var_count
|
289
|
+
|
290
|
+
p = @Product_klass.first
|
291
|
+
|
292
|
+
p.variants_including_master.should have_exactly(4).items
|
293
|
+
p.variants.should have_exactly(3).items
|
294
|
+
|
295
|
+
p.variants.each { |v| v.option_values.each {|o| puts o.inspect } }
|
296
|
+
|
297
|
+
p.option_types.each { |ot| puts ot.inspect }
|
298
|
+
p.option_types.should have_exactly(2).items # mime_type, print_type
|
299
|
+
|
300
|
+
v1 = p.variants[0]
|
301
|
+
v1.option_values.should have_exactly(2).items
|
302
|
+
v1.option_values.collect(&:name).sort.should == ['colour','jpeg']
|
303
|
+
|
304
|
+
end
|
305
|
+
|
262
306
|
##################
|
263
307
|
### PROPERTIES ###
|
264
308
|
##################
|
@@ -429,5 +473,6 @@ describe 'SpreeLoader' do
|
|
429
473
|
it "should raise exception when single mandatory column missing from .csv", :ex => true do
|
430
474
|
expect {@product_loader.perform_load($SpreeNegativeFixturePath + '/SpreeProdMiss1Mandatory.csv', :mandatory => 'sku' )}.to raise_error(DataShift::MissingMandatoryError)
|
431
475
|
end
|
476
|
+
|
432
477
|
|
433
478
|
end
|
data/tasks/db_tasks.rake
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
namespace :datashift do
|
2
|
+
|
3
|
+
namespace :db do
|
4
|
+
|
5
|
+
SYSTEM_TABLE_EXCLUSION_LIST = ['schema_migrations']
|
6
|
+
|
7
|
+
desc "Purge the current database"
|
8
|
+
task :purge, [:exclude_system_tables] => [:environment] do |t, args|
|
9
|
+
require 'highline/import'
|
10
|
+
|
11
|
+
if(Rails.env.production?)
|
12
|
+
agree("WARNING: In Production database, REALLY PURGE ? [y]:")
|
13
|
+
end
|
14
|
+
|
15
|
+
config = ActiveRecord::Base.configurations[Rails.env || 'development']
|
16
|
+
case config['adapter']
|
17
|
+
when "mysql", "mysql2", "jdbcmysql"
|
18
|
+
ActiveRecord::Base.establish_connection(config)
|
19
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
20
|
+
next if(args[:exclude_system_tables] && SYSTEM_TABLE_EXCLUSION_LIST.include?(table) )
|
21
|
+
puts "purging table: #{table}"
|
22
|
+
ActiveRecord::Base.connection.execute("TRUNCATE #{table}")
|
23
|
+
end
|
24
|
+
when "sqlite","sqlite3"
|
25
|
+
dbfile = config["database"] || config["dbfile"]
|
26
|
+
File.delete(dbfile) if File.exist?(dbfile)
|
27
|
+
when "sqlserver"
|
28
|
+
dropfkscript = "#{config["host"]}.#{config["database"]}.DP1".gsub(/\\/,'-')
|
29
|
+
`osql -E -S #{config["host"]} -d #{config["database"]} -i db\\#{dropfkscript}`
|
30
|
+
`osql -E -S #{config["host"]} -d #{config["database"]} -i db\\#{Rails.env}_structure.sql`
|
31
|
+
when "oci", "oracle"
|
32
|
+
ActiveRecord::Base.establish_connection(config)
|
33
|
+
ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl|
|
34
|
+
ActiveRecord::Base.connection.execute(ddl)
|
35
|
+
end
|
36
|
+
when "firebird"
|
37
|
+
ActiveRecord::Base.establish_connection(config)
|
38
|
+
ActiveRecord::Base.connection.recreate_database!
|
39
|
+
else
|
40
|
+
raise "Task not supported by '#{config["adapter"]}'"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|