ar_loader 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Binary file
Binary file
@@ -0,0 +1,61 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2010
4
+ # License:: MIT
5
+ #
6
+ class LoaderBase
7
+
8
+ attr_accessor :load_object, :value
9
+
10
+ # Enable single column (association) to contain multiple name/value sets in default form :
11
+ # Name1:value1, value2|Name2:value1, value2, value3|Name3:value1, value2
12
+ #
13
+ # E.G.
14
+ # Row for association could have a name (Size/Colour/Sex) with a set of values,
15
+ # and this combination can be expressed multiple times :
16
+ # Size:small,medium,large|Colour:red, green|Sex:Female
17
+
18
+ @@name_value_delim = ':'
19
+ @@multi_value_delim = ','
20
+ @@multi_assoc_delim = '|'
21
+
22
+ def self.set_name_value_delim(x) @@name_value_delim = x; end
23
+ def self.set_multi_value_delim(x) @@multi_value_delim = x; end
24
+ def self.set_multi_assoc_delim(x) @@multi_assoc_delim = x; end
25
+
26
+ def initialize(object)
27
+ @load_object = object
28
+ end
29
+
30
+ # What process a value string from a column.
31
+ # Assigning value(s) to correct association on @load_object.
32
+ # Method map represents a column from a file and it's correlated AR associations.
33
+ # Value string which may contain multiple values for a collection association.
34
+ #
35
+ def process(method_map, value)
36
+ @value = value
37
+
38
+ if(method_map.has_many && method_map.has_many_class && @value)
39
+ # The Generic handler for Associations
40
+ # The actual class of the association so we can find_or_create on it
41
+ assoc_class = method_map.has_many_class
42
+
43
+ puts "Processing Association: #{assoc_class} : #{@value}"
44
+
45
+ @value.split(@@multi_assoc_delim).collect do |lookup|
46
+ # TODO - Don't just rely on 'name' but try different finds as per MethodMappe::insistent_belongs_to ..
47
+ x = assoc_class.find(:first, :conditions => ['lower(name) LIKE ?', "%#{lookup.downcase}%"])
48
+ unless x
49
+ puts "WARNING: #{lookup} in #{assoc_class} NOT found - Not added to #{@load_object.class}"
50
+ next
51
+ end
52
+ @load_object.send( method_map.has_many ) << x
53
+ @load_object.save
54
+ end
55
+ else
56
+ # Nice n simple straight assignment to a column variable
57
+ method_map.assign(@load_object, @value) unless method_map.has_many
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,47 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Jan 2011
4
+ # License:: MIT. Free, Open Source.
5
+ #
6
+ require 'loader_base'
7
+
8
+ class ImageLoader < LoaderBase
9
+
10
+ def initialize(image = nil)
11
+ obj = image || Image.create
12
+ super( obj )
13
+ raise "Failed to create Image for loading" unless @load_object
14
+ end
15
+
16
+ def refresh
17
+ @load_object = Image.create
18
+ end
19
+
20
+ # Note the Spree Image model sets default storage path to
21
+ # => :path => ":rails_root/public/assets/products/:id/:style/:basename.:extension"
22
+
23
+ def process( image_path, record = nil)
24
+
25
+ unless File.exists?(image_path)
26
+ puts "ERROR : Invalid Path"
27
+ return
28
+ end
29
+
30
+ alt = (record and record.respond_to? :name) ? record.name : ""
31
+
32
+ @load_object.alt = alt
33
+
34
+ begin
35
+ @load_object.attachment = File.new(image_path, "r")
36
+ rescue => e
37
+ puts e.inspect
38
+ puts "ERROR : Failed to read image #{image_path}"
39
+ return
40
+ end
41
+
42
+ @load_object.attachment.reprocess!
43
+ @load_object.viewable = record if record
44
+
45
+ puts @load_object.save ? "Success: Uploaded Image: #{@load_object.inspect}" : "ERROR : Problem saving to DB Image: #{@load_object}"
46
+ end
47
+ end
@@ -0,0 +1,93 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2010
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2010
4
+ # License:: MIT ?
5
+ #
6
+ # Details:: Specific over-rides/additions to general loader to
7
+ # support Spree Products
8
+ #
9
+ require 'loader_base'
10
+
11
+ class ProductLoader < LoaderBase
12
+
13
+ def initialize(product = nil)
14
+ prod = product || Product.create
15
+ super( prod )
16
+ raise "Failed to create Product for loading" unless @load_object
17
+ end
18
+
19
+ # What process a value string from a column, assigning value(s) to correct association on Product.
20
+ # Method map represents a column from a file and it's correlated Product association.
21
+ # Value string which may contain multiple values for a collection association.
22
+ # Product to assign that value to.
23
+ def process( method_map, value)
24
+ @value = value
25
+
26
+ #puts "DEBUG : process #{method_map.inspect} : #{value.inspect}"
27
+ # Special case for OptionTypes as it's two stage process
28
+ # First add the possible option_types to Product, then we are able
29
+ # to define Variants on those options.
30
+
31
+ if(method_map.name == 'option_types' && @value)
32
+
33
+ option_types = @value.split(@@multi_assoc_delim)
34
+ option_types.each do |ostr|
35
+ oname, value_str = ostr.split(@@name_value_delim)
36
+ option_type = OptionType.find_or_create_by_name(oname)
37
+ unless option_type
38
+ puts "WARNING: OptionType #{oname} NOT found - Not set Product"
39
+ next
40
+ end
41
+
42
+ @load_object.option_types << option_type unless @load_object.option_types.include?(option_type)
43
+
44
+ # Now get the value(s) for the option e.g red,blue,green for OptType 'colour'
45
+ ovalues = value_str.split(',')
46
+ ovalues.each_with_index do |ovname, i|
47
+ ovname.strip!
48
+ ov = OptionValue.find_by_name(ovname)
49
+ if ov
50
+ object = Variant.new( :sku => "#{@load_object.sku}_#{i}", :price => @load_object.price, :available_on => @load_object.available_on)
51
+ #puts "DEBUG: Create New Variant: #{object.inspect}"
52
+ object.option_values << ov
53
+ @load_object.variants << object
54
+ else
55
+ puts "WARNING: Option #{ovname} NOT FOUND - No Variant created"
56
+ end
57
+ end
58
+ end
59
+
60
+ # Special case for ProductProperties since it can have additional value applied.
61
+ # A list of Properties with a optional Value - supplied in form :
62
+ # Property:value|Property2:value|Property3:value
63
+ #
64
+ elsif(method_map.name == 'product_properties' && @value)
65
+
66
+ property_list = @value.split(@@multi_assoc_delim)
67
+
68
+ property_list.each do |pstr|
69
+ pname, pvalue = pstr.split(@@name_value_delim)
70
+ property = Property.find_by_name(pname)
71
+ unless property
72
+ puts "WARNING: Property #{pname} NOT found - Not set Product"
73
+ next
74
+ end
75
+ @load_object.product_properties << ProductProperty.create( :property => property, :value => pvalue)
76
+ end
77
+
78
+ elsif(method_map.name == 'count_on_hand' && @load_object.variants.size > 0 &&
79
+ @value.is_a?(String) && @value.include?(@@multi_assoc_delim))
80
+ # Check if we processed Option Types and assign count per option
81
+ values = @value.split(@@multi_assoc_delim)
82
+ if(@load_object.variants.size == values.size)
83
+ @load_object.variants.each_with_index {|v, i| v.count_on_hand == values[i] }
84
+ else
85
+ puts "WARNING: Count on hand entries does not match number of Variants"
86
+ end
87
+
88
+ else
89
+ super(method_map, value)
90
+ end
91
+
92
+ end
93
+ end
data/lib/to_b.rb ADDED
@@ -0,0 +1,24 @@
1
+ class Object
2
+ def to_b
3
+ case self
4
+ when true, false: self
5
+ when nil: false
6
+ else
7
+ to_i != 0
8
+ end
9
+ end
10
+ end
11
+
12
+ class String
13
+ TRUE_REGEXP = /^(yes|true|on|t|1|\-1)$/i.freeze
14
+ FALSE_REGEXP = /^(no|false|off|f|0)$/i.freeze
15
+
16
+ def to_b
17
+ case self
18
+ when TRUE_REGEXP: true
19
+ when FALSE_REGEXP: false
20
+ else
21
+ to_i != 0
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,138 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe 'ExcelLoader' do
4
+
5
+ before do
6
+ @klazz = Product
7
+ MethodMapper.clear
8
+ end
9
+
10
+ it "should populate operators for a given AR model" do
11
+ MethodMapper.find_operators( @klazz )
12
+
13
+ MethodMapper.has_many.should_not be_empty
14
+ MethodMapper.assignments.should_not be_empty
15
+
16
+ hmf = MethodMapper.has_many_for(@klazz)
17
+ arf = MethodMapper.assignments_for(@klazz)
18
+
19
+ (hmf & arf).should_not be_empty # Associations provide << or =
20
+
21
+ hmf.should include('properties')
22
+ arf.should include('count_on_hand') # example of a column
23
+ arf.should include('cost_price') # example of delegated assignment (available through Variant)
24
+
25
+ MethodMapper.column_types.should be_is_a(Hash)
26
+ MethodMapper.column_types.should_not be_empty
27
+
28
+ MethodMapper.column_type_for(@klazz, 'count_on_hand').should_not be_nil
29
+ end
30
+
31
+ it "should populate operators respecting unique option" do
32
+ MethodMapper.find_operators( @klazz, :unique => true )
33
+
34
+ hmf = MethodMapper.has_many_for(@klazz)
35
+ arf = MethodMapper.assignments_for(@klazz)
36
+
37
+ (hmf & arf).should be_empty
38
+ end
39
+
40
+ it "should populate assignment method and col type for different forms of a column name" do
41
+
42
+ MethodMapper.find_operators( @klazz )
43
+
44
+ ["Count On hand", 'count_on_hand', "Count OnHand", "COUNT ONHand"].each do |format|
45
+ mmap = MethodMapper.determine_calls( @klazz, format )
46
+
47
+ mmap.class.should == MethodDetail
48
+
49
+ mmap.assignment.should == 'count_on_hand='
50
+ mmap.has_many.should be_nil
51
+
52
+ mmap.col_type.should_not be_nil
53
+ mmap.col_type.name.should == 'count_on_hand'
54
+ mmap.col_type.default.should == 0
55
+ mmap.col_type.sql_type.should == 'int(10)'
56
+ mmap.col_type.type.should == :integer
57
+ end
58
+ end
59
+
60
+ it "should populate both methods for different forms of an association name" do
61
+
62
+ MethodMapper.find_operators( @klazz )
63
+ ["product_option_types", "product option types", 'product Option_types', "ProductOptionTypes", "Product_Option_Types"].each do |format|
64
+ mmap = MethodMapper.determine_calls( @klazz, format )
65
+
66
+ mmap.assignment.should == 'product_option_types='
67
+ mmap.has_many.should == 'product_option_types'
68
+
69
+ mmap.col_type.should be_nil
70
+ end
71
+ end
72
+
73
+
74
+ it "should not populate anything when non existent column name" do
75
+ ["On sale", 'on_sale'].each do |format|
76
+ mmap = MethodMapper.determine_calls( @klazz, format )
77
+
78
+ mmap.class.should == MethodDetail
79
+ mmap.assignment.should be_nil
80
+ mmap.has_many.should be_nil
81
+ mmap.col_type.should be_nil
82
+ end
83
+ end
84
+
85
+ it "should enable correct assignment and sending of a value to AR model" do
86
+
87
+ MethodMapper.find_operators( @klazz )
88
+
89
+ mmap = MethodMapper.determine_calls( @klazz, 'count on hand' )
90
+ mmap.assignment.should == 'count_on_hand='
91
+
92
+ x = @klazz.new
93
+
94
+ x.should be_new_record
95
+
96
+ x.send( mmap.assignment, 2 )
97
+ x.count_on_hand.should == 2
98
+ x.on_hand.should == 2 # helper method I know looks at same thing
99
+
100
+ mmap = MethodMapper.determine_calls( @klazz, 'SKU' )
101
+ mmap.assignment.should == 'sku='
102
+ x.send( mmap.assignment, 'TEST_SK 001' )
103
+ x.sku.should == 'TEST_SK 001'
104
+ end
105
+
106
+ it "should enable correct assignment and sending of association to AR model" do
107
+
108
+ MethodMapper.find_operators( @klazz )
109
+
110
+ mmap = MethodMapper.determine_calls( @klazz, 'taxons' )
111
+ mmap.has_many.should == 'taxons'
112
+
113
+ x = @klazz.new
114
+
115
+ # NEW ASSOCIATION ASSIGNMENT v.,mn
116
+ x.send( mmap.has_many ) << Taxon.new
117
+ x.taxons.size.should == 1
118
+
119
+ x.send( mmap.has_many ) << [Taxon.new, Taxon.new]
120
+ x.taxons.size.should == 3
121
+
122
+ # EXISTING ASSOCIATIONS
123
+ x = Product.find :first
124
+
125
+ t = Taxonomy.find_or_create_by_name( 'BlahSpecTest' )
126
+
127
+ if x
128
+ sz = x.taxons.size
129
+ x.send(mmap.has_many) << t.root
130
+ x.taxons.size.should == sz + 1
131
+ else
132
+ puts "WARNING : Test not run could not find any Test Products"
133
+ end
134
+
135
+ end
136
+
137
+
138
+ end
@@ -0,0 +1,37 @@
1
+ unless defined? SPREE_ROOT
2
+ ENV["RAILS_ENV"] = "test"
3
+ case
4
+ when ENV["SPREE_ENV_FILE"]
5
+ require ENV["SPREE_ENV_FILE"]
6
+ when File.dirname(__FILE__) =~ %r{vendor/SPREE/vendor/extensions}
7
+ require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../../")}/config/environment"
8
+ else
9
+ require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../")}/config/environment"
10
+ end
11
+ end
12
+ require "#{SPREE_ROOT}/spec/spec_helper"
13
+
14
+ if File.directory?(File.dirname(__FILE__) + "/scenarios")
15
+ Scenario.load_paths.unshift File.dirname(__FILE__) + "/scenarios"
16
+ end
17
+ if File.directory?(File.dirname(__FILE__) + "/matchers")
18
+ Dir[File.dirname(__FILE__) + "/matchers/*.rb"].each {|file| require file }
19
+ end
20
+
21
+ Spec::Runner.configure do |config|
22
+ # config.use_transactional_fixtures = true
23
+ # config.use_instantiated_fixtures = false
24
+ # config.fixture_path = RAILS_ROOT + '/spec/fixtures'
25
+
26
+ # You can declare fixtures for each behaviour like this:
27
+ # describe "...." do
28
+ # fixtures :table_a, :table_b
29
+ #
30
+ # Alternatively, if you prefer to declare them only once, you can
31
+ # do so here, like so ...
32
+ #
33
+ # config.global_fixtures = :table_a, :table_b
34
+ #
35
+ # If you declare global fixtures, be aware that they will be declared
36
+ # for all of your examples, even those that don't use them.
37
+ end
@@ -0,0 +1,65 @@
1
+ # Author :: Tom Statter
2
+ # Date :: Mar 2011
3
+ #
4
+ # License:: The MIT License (Free and OpenSource)
5
+ #
6
+ # About:: Additional Rake tasks useful when testing seeding DB via ARLoader
7
+ #
8
+ namespace :autotelik do
9
+
10
+ namespace :db do
11
+
12
+ SYSTEM_TABLE_EXCLUSION_LIST = ['schema_migrations']
13
+
14
+ desc "Purge the current database"
15
+ task :purge, :exclude_system_tables, :needs => [:environment] do |t, args|
16
+ require 'highline/import'
17
+
18
+ if(RAILS_ENV == 'production')
19
+ agree("WARNING: In Production database, REALLY PURGE ? [y]:")
20
+ end
21
+
22
+ config = ActiveRecord::Base.configurations[RAILS_ENV || 'development']
23
+ case config['adapter']
24
+ when "mysql", "jdbcmysql"
25
+ ActiveRecord::Base.establish_connection(config)
26
+ ActiveRecord::Base.connection.tables.each do |table|
27
+ next if(args[:exclude_system_tables] && SYSTEM_TABLE_EXCLUSION_LIST.include?(table) )
28
+ puts "purging table: #{table}"
29
+ ActiveRecord::Base.connection.execute("TRUNCATE #{table}")
30
+ end
31
+ when "sqlite","sqlite3"
32
+ dbfile = config["database"] || config["dbfile"]
33
+ File.delete(dbfile) if File.exist?(dbfile)
34
+ when "sqlserver"
35
+ dropfkscript = "#{config["host"]}.#{config["database"]}.DP1".gsub(/\\/,'-')
36
+ `osql -E -S #{config["host"]} -d #{config["database"]} -i db\\#{dropfkscript}`
37
+ `osql -E -S #{config["host"]} -d #{config["database"]} -i db\\#{RAILS_ENV}_structure.sql`
38
+ when "oci", "oracle"
39
+ ActiveRecord::Base.establish_connection(config)
40
+ ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl|
41
+ ActiveRecord::Base.connection.execute(ddl)
42
+ end
43
+ when "firebird"
44
+ ActiveRecord::Base.establish_connection(config)
45
+ ActiveRecord::Base.connection.recreate_database!
46
+ else
47
+ raise "Task not supported by '#{config["adapter"]}'"
48
+ end
49
+ end
50
+
51
+ desc "Clear database and optional directories such as assets, then run db:seed"
52
+ task :seed_again, :assets, :needs => [:environment] do |t, args|
53
+
54
+ Rake::Task['autotelik:db:purge'].invoke( true ) # i.e ENV['exclude_system_tables'] = true
55
+
56
+ if(args[:assets])
57
+ assets = "#{Rails.root}/public/assets"
58
+ FileUtils::rm_rf(assets) if(File.exists?(assets))
59
+ end
60
+
61
+ Rake::Task['db:seed'].invoke
62
+ end
63
+
64
+ end # db
65
+ end # autotelik
@@ -0,0 +1,101 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Feb 2011
4
+ # License:: TBD. Free, Open Source. MIT ?
5
+ #
6
+ # REQUIRES: JRuby
7
+ #
8
+ # Usage from rake : jruby -S rake excel_loader input=<file.xls>
9
+ #
10
+ # e.g. => jruby -S rake excel_load input=vendor\extensions\autotelik\fixtures\ExampleInfoWeb.xls
11
+ # => jruby -S rake excel_load input=C:\MyProducts.xls verbose=true
12
+ #
13
+ namespace :autotelik do
14
+
15
+ desc "Populate AR model's table with data stored in Excel"
16
+ task :excel_load, :klass, :input, :verbose, :sku_prefix, :needs => :environment do |t, args|
17
+
18
+ raise "USAGE: jruby -S rake excel_load input=excel_file.xls" unless args[:input]
19
+ raise "ERROR: Cannot process without AR Model - please supply model=<Class>" unless args[:class]
20
+ raise "ERROR: Could not find file #{args[:input]}" unless File.exists?(args[:input])
21
+
22
+ klass = Kernal.const_get(args[:model])
23
+ raise "ERROR: No such AR Model found - please check model=<Class>" unless(klass)
24
+
25
+ require 'product_loader'
26
+ require 'method_mapper_excel'
27
+
28
+ args[:class]
29
+
30
+ @method_mapper = MethodMapperExcel.new(args[:input], Product)
31
+
32
+ @excel = @method_mapper.excel
33
+
34
+ if(args[:verbose])
35
+ puts "Loading from Excel file: #{args[:input]}"
36
+ puts "Processing #{@excel.num_rows} rows"
37
+ end
38
+
39
+ # TODO create YAML configuration file to drive mandatory columns
40
+ #
41
+ # TODO create YAML configuration file to drive defaults etc
42
+
43
+ # Process spreadsheet and create model instances
44
+
45
+ method_names = @method_mapper.method_names
46
+
47
+ Product.transaction do
48
+ @products = []
49
+
50
+ (1..@excel.num_rows).collect do |row|
51
+
52
+ product_data_row = @excel.sheet.getRow(row)
53
+ break if product_data_row.nil?
54
+
55
+ # Excel num_rows seems to return all 'visible' rows so,
56
+ # we have to manually detect when actual data ends and all the empty rows start
57
+ contains_data = required_methods.find { |mthd| ! product_data_row.getCell(method_names.index(mthd)).to_s.empty? }
58
+ break unless contains_data
59
+
60
+ @assoc_classes = {}
61
+
62
+ loader = ProductLoader.new()
63
+
64
+ # TODO - Smart sorting of column processing order ....
65
+ # Does not currently ensure mandatory columns (for valid?) processed first but model needs saving
66
+ # before associations can be processed so user should ensure mandatory columns are prior to associations
67
+
68
+ @method_mapper.methods.each_with_index do |method_map, col|
69
+
70
+ loader.process(method_map, @excel.value(product_data_row, col))
71
+ begin
72
+ loader.load_object.save if( loader.load_object.valid? && loader.load_object.new_record? )
73
+ rescue
74
+ raise "Error processing Product"
75
+ end
76
+ end
77
+
78
+ product = loader.load_object
79
+
80
+ product.available_on ||= Time.now.to_s(:db)
81
+
82
+ # TODO - handle when it's not valid ?
83
+ # Process rest and dump out an exception list of Products
84
+ #unless(product.valid?)
85
+ #end
86
+
87
+ puts "SAVING ROW #{row} : #{product.inspect}" if args[:verbose]
88
+
89
+ unless(product.save)
90
+ puts product.errors.inspect
91
+ puts product.errors.full_messages.inspect
92
+ raise "Error Saving Product: #{product.sku} :#{product.name}"
93
+ else
94
+ @products << product
95
+ end
96
+ end
97
+ end # TRANSACTION
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,38 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Feb 2011
4
+ # License:: MIT. Free, Open Source.
5
+ #
6
+ # Usage:: rake autotelik:file_rename input=/blah image_load input=path_to_images
7
+ #
8
+
9
+ namespace :autotelik do
10
+
11
+ desc "copy or mv a folder of files, consistently renaming in the process"
12
+ task :file_rename, :input, :offset, :prefix, :width, :commit, :mv do |t, args|
13
+ raise "USAGE: rake file_rename input='C:\blah' [offset=n prefix='str' width=n]" unless args[:input] && File.exists?(args[:input])
14
+ width = args[:width] || 2
15
+
16
+ action = args[:mv] ? 'mv' : 'cp'
17
+
18
+ cache = args[:input]
19
+
20
+ if(File.exists?(cache) )
21
+ puts "Renaming files from #{cache}"
22
+ Dir.glob(File.join(cache, "*")) do |name|
23
+ path, base_name = File.split(name)
24
+ id = base_name.slice!(/\w+/)
25
+
26
+ id = id.to_i + args[:offset].to_i if(args[:offset])
27
+ id = "%0#{width}d" % id.to_i if(args[:width])
28
+ id = args[:prefix] + id.to_s if(args[:prefix])
29
+
30
+ destination = File.join(path, "#{id}#{base_name}")
31
+ puts "ACTION: #{action} #{name} #{destination}"
32
+
33
+ File.send( action, name, destination) if args[:commit]
34
+ end
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,15 @@
1
+ p = Product.seed( :name ) do |s|
2
+ s.name = <%= @name %>
3
+ s.available_on = '2009-09-01 09:00:00.0'
4
+ s.meta_keywords = ['training', 'training']
5
+ s.meta_description = ""
6
+ s.description = '<%= @description %>'
7
+ s.price = 0.00
8
+ s.sku = '<%= @sku %>'
9
+
10
+ s.is_physical = false
11
+ s.is_private = false
12
+
13
+ s.append_association :taxons, Taxon.find_by_name( 'Training' )
14
+
15
+ end