datashift 0.0.2 → 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 (35) hide show
  1. data/README.markdown +13 -12
  2. data/Rakefile +8 -8
  3. data/VERSION +1 -5
  4. data/datashift.gemspec +12 -38
  5. data/lib/applications/jruby/jexcel_file.rb +23 -11
  6. data/lib/datashift/method_detail.rb +44 -5
  7. data/lib/datashift/method_dictionary.rb +210 -0
  8. data/lib/datashift/method_mapper.rb +25 -191
  9. data/lib/generators/excel_generator.rb +12 -11
  10. data/lib/helpers/spree_helper.rb +36 -12
  11. data/lib/loaders/excel_loader.rb +2 -1
  12. data/lib/loaders/loader_base.rb +37 -20
  13. data/lib/loaders/spree/image_loader.rb +35 -16
  14. data/lib/loaders/spree/product_loader.rb +27 -1
  15. data/spec/csv_loader_spec.rb +3 -3
  16. data/spec/excel_exporter_spec.rb +79 -0
  17. data/spec/excel_generator_spec.rb +3 -3
  18. data/spec/excel_loader_spec.rb +35 -16
  19. data/spec/fixtures/ProjectsMultiCategoriesHeaderLookup.xls +0 -0
  20. data/spec/fixtures/images/DEMO_001_ror_bag.jpeg +0 -0
  21. data/spec/fixtures/images/DEMO_002_Powerstation.jpg +0 -0
  22. data/spec/fixtures/images/DEMO_003_ror_mug.jpeg +0 -0
  23. data/spec/fixtures/images/DEMO_004_ror_ringer.jpeg +0 -0
  24. data/spec/fixtures/interact_models_db.sqlite +0 -0
  25. data/spec/fixtures/interact_spree_db.sqlite +0 -0
  26. data/spec/fixtures/spree/SpreeProductsWithImages.xls +0 -0
  27. data/spec/loader_spec.rb +4 -4
  28. data/spec/method_dictionary_spec.rb +243 -0
  29. data/spec/method_mapper_spec.rb +17 -213
  30. data/spec/spec_helper.rb +1 -0
  31. data/spec/spree_loader_spec.rb +18 -1
  32. data/spec/spree_method_mapping_spec.rb +4 -4
  33. metadata +14 -130
  34. data/Gemfile +0 -28
  35. data/spec/fixtures/.~lock.ProjectsSingleCategories.xls# +0 -1
@@ -7,39 +7,58 @@ require 'loader_base'
7
7
 
8
8
  module DataShift
9
9
 
10
- class ImageLoader < LoaderBase
11
-
12
- def initialize(image = nil)
13
- super( Image, image )
14
- raise "Failed to create Image for loading" unless @load_object
15
- end
10
+ module ImageLoading
16
11
 
12
+
17
13
  # Note the Spree Image model sets default storage path to
18
14
  # => :path => ":rails_root/public/assets/products/:id/:style/:basename.:extension"
19
15
 
20
- def process( image_path, record = nil)
16
+ def create(image_path, viewable_record = nil, options = {})
21
17
 
18
+ image = Image.new
19
+
22
20
  unless File.exists?(image_path)
23
21
  puts "ERROR : Invalid Path"
24
- return
22
+ return image
25
23
  end
26
24
 
27
- alt = (record and record.respond_to? :name) ? record.name : ""
28
-
29
- @load_object.alt = alt
25
+ alt = if(options[:alt])
26
+ options[:alt]
27
+ else
28
+ (viewable_record and viewable_record.respond_to? :name) ? viewable_record.name : ""
29
+ end
30
+
31
+ image.alt = alt
30
32
 
31
33
  begin
32
- @load_object.attachment = File.new(image_path, "r")
34
+ image.attachment = File.new(image_path, "r")
33
35
  rescue => e
34
36
  puts e.inspect
35
37
  puts "ERROR : Failed to read image #{image_path}"
36
- return
38
+ return image
37
39
  end
38
40
 
39
- @load_object.attachment.reprocess!
40
- @load_object.viewable = record if record
41
+ image.attachment.reprocess!
42
+ image.viewable = viewable_record if viewable_record
43
+
44
+ puts image.save ? "Success: Crteated Image: #{image.inspect}" : "ERROR : Problem saving to DB Image: #{image.inspect}"
45
+ end
46
+ end
47
+
48
+ class ImageLoader < LoaderBase
49
+
50
+ include DataShift::ImageLoading
51
+
52
+ def initialize(image = nil)
53
+ super( Image, image )
54
+ raise "Failed to create Image for loading" unless @load_object
55
+ end
41
56
 
42
- puts @load_object.save ? "Success: Uploaded Image: #{@load_object.inspect}" : "ERROR : Problem saving to DB Image: #{@load_object}"
57
+ # Note the Spree Image model sets default storage path to
58
+ # => :path => ":rails_root/public/assets/products/:id/:style/:basename.:extension"
59
+
60
+ def process( image_path, record = nil)
61
+ @load_object = create_image(path, record)
43
62
  end
44
63
  end
45
64
 
@@ -6,8 +6,9 @@
6
6
  # Details:: Specific over-rides/additions to support Spree Products
7
7
  #
8
8
  require 'loader_base'
9
- require 'excel_loader'
10
9
  require 'csv_loader'
10
+ require 'excel_loader'
11
+ require 'image_loader'
11
12
 
12
13
  module DataShift
13
14
 
@@ -17,6 +18,7 @@ module DataShift
17
18
 
18
19
  include DataShift::CsvLoading
19
20
  include DataShift::ExcelLoading
21
+ include DataShift::ImageLoading
20
22
 
21
23
  def initialize(product = nil)
22
24
  super( Product, product, :instance_methods => true )
@@ -63,6 +65,10 @@ module DataShift
63
65
  elsif(@current_method_detail.operator?('product_properties') && current_value)
64
66
 
65
67
  add_properties
68
+
69
+ elsif(@current_method_detail.operator?('images') && current_value)
70
+
71
+ add_images
66
72
 
67
73
  elsif(@current_method_detail.operator?('count_on_hand') || @current_method_detail.operator?('on_hand') )
68
74
 
@@ -150,7 +156,27 @@ module DataShift
150
156
 
151
157
  end
152
158
 
159
+
160
+ # Special case for Images
161
+ # A list of paths to Images with a optional 'alt' value - supplied in form :
162
+ # path:alt|path2:alt2|path_3:alt3 etc
163
+ #
164
+ def add_images
165
+ # TODO smart column ordering to ensure always valid by time we get to associations
166
+ save_if_new
167
+
168
+ images = current_value.split(LoaderBase::multi_assoc_delim)
153
169
 
170
+ images.each do |image|
171
+
172
+ img_path, alt_text = image.split(LoaderBase::name_value_delim)
173
+
174
+ image = create_image(img_path, @load_object, :alt => alt_text)
175
+ end
176
+
177
+ end
178
+
179
+
154
180
  # Special case for ProductProperties since it can have additional value applied.
155
181
  # A list of Properties with a optional Value - supplied in form :
156
182
  # property.name:value|property.name|property.name:value
@@ -23,9 +23,9 @@ describe 'CSV Loader' do
23
23
  end
24
24
 
25
25
  before(:each) do
26
- MethodMapper.clear
27
- MethodMapper.find_operators( @klazz )
28
- MethodMapper.find_operators( @assoc_klazz )
26
+ MethodDictionary.clear
27
+ MethodDictionary.find_operators( @klazz )
28
+ MethodDictionary.find_operators( @assoc_klazz )
29
29
  end
30
30
 
31
31
  end
@@ -0,0 +1,79 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2011
4
+ # License:: MIT
5
+ #
6
+ # Details:: Specs for Excel aspect of Active Record Loader
7
+ #
8
+ require File.dirname(__FILE__) + '/spec_helper'
9
+
10
+ if(Guards::jruby?)
11
+ require 'erb'
12
+ require 'excel_generator'
13
+
14
+ include DataShift
15
+
16
+ describe 'Excel Generator' do
17
+
18
+ before(:all) do
19
+ db_connect( 'test_file' ) # , test_memory, test_mysql
20
+
21
+ # load our test model definitions - Project etc
22
+ require File.join($DataShiftFixturePath, 'test_model_defs')
23
+
24
+ # handle migration changes or reset of test DB
25
+ migrate_up
26
+
27
+ db_clear() # todo read up about proper transactional fixtures
28
+ results_clear()
29
+
30
+ @klazz = Project
31
+ @assoc_klazz = Category
32
+ end
33
+
34
+ before(:each) do
35
+ MethodDictionary.clear
36
+ MethodDictionary.find_operators( @klazz )
37
+ MethodDictionary.find_operators( @assoc_klazz )
38
+ end
39
+
40
+ it "should be able to create a new excel generator" do
41
+ generator = ExcelGenerator.new( 'dummy.xls' )
42
+
43
+ generator.should_not be_nil
44
+ end
45
+
46
+ it "should generate template .xls file from model" do
47
+
48
+ expect = result_file('project_template_spec.xls')
49
+
50
+ gen = ExcelGenerator.new( expect )
51
+
52
+ gen.generate(Project)
53
+
54
+ File.exists?(expect).should be_true
55
+
56
+ puts "Can manually check file @ #{expect}"
57
+ end
58
+
59
+ it "should export a simple model to .xls spreedsheet" do
60
+
61
+ Project.create( :value_as_string => 'Value as Text', :value_as_boolean => true, :value_as_double => 75.672)
62
+ #001 Demo string blah blah 2011-02-14 1.00 320.00
63
+
64
+ expect= result_file('simple_export_spec.xls')
65
+
66
+ gen = ExcelGenerator.new(expect)
67
+
68
+ items = Project.all
69
+
70
+ gen.export(items)
71
+
72
+ File.exists?(expect).should be_true
73
+
74
+ end
75
+
76
+ end
77
+ else
78
+ puts "WARNING: skipped excel_generator_spec : Requires JRUBY - JExcelFile requires JAVA"
79
+ end # jruby
@@ -32,9 +32,9 @@ if(Guards::jruby?)
32
32
  end
33
33
 
34
34
  before(:each) do
35
- MethodMapper.clear
36
- MethodMapper.find_operators( @klazz )
37
- MethodMapper.find_operators( @assoc_klazz )
35
+ MethodDictionary.clear
36
+ MethodDictionary.find_operators( @klazz )
37
+ MethodDictionary.find_operators( @assoc_klazz )
38
38
  end
39
39
 
40
40
  it "should be able to create a new excel generator" do
@@ -28,7 +28,7 @@ if(Guards::jruby?)
28
28
  db_clear() # todo read up about proper transactional fixtures
29
29
 
30
30
 
31
- @klazz = Project
31
+ Project = Project
32
32
  @assoc_klazz = Category
33
33
  end
34
34
 
@@ -36,49 +36,49 @@ if(Guards::jruby?)
36
36
 
37
37
  Project.delete_all
38
38
 
39
- %w{category_001 category_002 category_003}.each do |cat|
39
+ %w{category_001 category_002 category_003 category_004 category_005}.each do |cat|
40
40
  @assoc_klazz.find_or_create_by_reference(cat)
41
41
  end
42
42
 
43
43
 
44
- MethodMapper.clear
45
- MethodMapper.find_operators( @klazz )
46
- MethodMapper.find_operators( @assoc_klazz )
44
+ MethodDictionary.clear
45
+ MethodDictionary.find_operators( Project )
46
+ MethodDictionary.find_operators( @assoc_klazz )
47
47
  end
48
48
 
49
49
  it "should be able to create a new excel loader and load object" do
50
- loader = ExcelLoader.new( @klazz)
50
+ loader = ExcelLoader.new( Project)
51
51
 
52
52
  loader.load_object.should_not be_nil
53
- loader.load_object.should be_is_a(@klazz)
53
+ loader.load_object.should be_is_a(Project)
54
54
  loader.load_object.new_record?.should be_true
55
55
  end
56
56
 
57
57
  it "should process a simple .xls spreedsheet" do
58
58
 
59
- loader = ExcelLoader.new(@klazz)
59
+ loader = ExcelLoader.new(Project)
60
60
 
61
- count = @klazz.count
61
+ count = Project.count
62
62
  loader.perform_load( $DataShiftFixturePath + '/SimpleProjects.xls')
63
63
 
64
- loader.loaded_count.should == (@klazz.count - count)
64
+ loader.loaded_count.should == (Project.count - count)
65
65
 
66
66
  end
67
67
 
68
68
  it "should process multiple associationss from single column" do
69
69
 
70
- @klazz.find_by_title('001').should be_nil
71
- count = @klazz.count
70
+ Project.find_by_title('001').should be_nil
71
+ count = Project.count
72
72
 
73
- loader = ExcelLoader.new(@klazz)
73
+ loader = ExcelLoader.new(Project)
74
74
 
75
75
  loader.perform_load( $DataShiftFixturePath + '/ProjectsSingleCategories.xls')
76
76
 
77
77
  loader.loaded_count.should be > 3
78
- loader.loaded_count.should == (@klazz.count - count)
78
+ loader.loaded_count.should == (Project.count - count)
79
79
 
80
80
  {'001' => 2, '002' => 1, '003' => 3, '099' => 0 }.each do|title, expected|
81
- project = @klazz.find_by_title(title)
81
+ project = Project.find_by_title(title)
82
82
 
83
83
  project.should_not be_nil
84
84
  puts "#{project.inspect} [#{project.categories.size}]"
@@ -97,7 +97,7 @@ if(Guards::jruby?)
97
97
  loader.loaded_count.should == (Project.count - count)
98
98
 
99
99
  {'004' => 3, '005' => 1, '006' => 0, '007' => 1 }.each do|title, expected|
100
- project = @klazz.find_by_title(title)
100
+ project = Project.find_by_title(title)
101
101
 
102
102
  project.should_not be_nil
103
103
 
@@ -106,6 +106,25 @@ if(Guards::jruby?)
106
106
 
107
107
  end
108
108
 
109
+ it "should process multiple associations with lookup specified in column from excel spreedsheet", :fail => true do
110
+
111
+ loader = ExcelLoader.new(Project)
112
+
113
+ count = Project.count
114
+ loader.perform_load( $DataShiftFixturePath + '/ProjectsMultiCategoriesHeaderLookup.xls')
115
+
116
+ loader.loaded_count.should == (Project.count - count)
117
+
118
+ {'004' => 4, '005' => 1, '006' => 0, '007' => 1 }.each do|title, expected|
119
+ project = Project.find_by_title(title)
120
+
121
+ project.should_not be_nil
122
+
123
+ project.should have(expected).categories
124
+ end
125
+
126
+ end
127
+
109
128
  it "should process excel spreedsheet with extra undefined columns" do
110
129
  loader = ExcelLoader.new(Project)
111
130
  lambda {loader.perform_load( ifixture_file('BadAssociationName.xls') ) }.should_not raise_error
Binary file
data/spec/loader_spec.rb CHANGED
@@ -15,15 +15,15 @@ describe 'Basic Loader' do
15
15
  db_connect( 'test_file' ) # , test_memory, test_mysql
16
16
 
17
17
  # load our test model definitions - Project etc
18
- require File.join($DataShiftFixturePath, 'test_model_defs')
19
-
18
+ require File.join($DataShiftFixturePath, 'test_model_defs')
19
+
20
20
  migrate_up
21
21
  @klazz = Project
22
22
  end
23
23
 
24
24
  before(:each) do
25
- MethodMapper.clear
26
- MethodMapper.find_operators( @klazz )
25
+ MethodDictionary.clear
26
+ MethodDictionary.find_operators( @klazz )
27
27
  end
28
28
 
29
29
  it "should be able to create a new loader and load object" do
@@ -0,0 +1,243 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2011
4
+ # License:: MIT
5
+ #
6
+ # Details:: Specs for MethodMapper aspect of Active Record Loader
7
+ # MethodMapper provides the bridge between 'strings' e.g column headings
8
+ # and a classes different types of assignment operators
9
+ #
10
+ require File.dirname(__FILE__) + '/spec_helper'
11
+
12
+ describe 'Method Mapping' do
13
+
14
+ before(:all) do
15
+ db_connect( 'test_file' ) # , test_memory, test_mysql
16
+
17
+ # load our test model definitions - Project etc
18
+ require ifixture_file('test_model_defs')
19
+
20
+ migrate_up
21
+ end
22
+
23
+ before(:each) do
24
+ MethodDictionary.clear
25
+ MethodDictionary.find_operators( Project )
26
+ MethodDictionary.find_operators( Milestone )
27
+ end
28
+
29
+ it "should store dictionary for multiple AR models" do
30
+ MethodDictionary.assignments.size.should == 2
31
+ MethodDictionary.has_many.size.should == 2
32
+ end
33
+
34
+ it "should populate method dictionary for a given AR model" do
35
+
36
+ MethodDictionary.has_many.should_not be_empty
37
+ MethodDictionary.has_many[Project].should include('milestones')
38
+
39
+ MethodDictionary.assignments.should_not be_empty
40
+ MethodDictionary.assignments[Project].should include('id')
41
+ MethodDictionary.assignments[Project].should include('value_as_string')
42
+ MethodDictionary.assignments[Project].should include('value_as_text')
43
+
44
+ MethodDictionary.belongs_to.should_not be_empty
45
+ MethodDictionary.belongs_to[Project].should be_empty
46
+
47
+
48
+ MethodDictionary.column_types.should be_is_a(Hash)
49
+ MethodDictionary.column_types.should_not be_empty
50
+ MethodDictionary.column_types[Project].size.should == Project.columns.size
51
+
52
+
53
+ end
54
+
55
+ it "should populate assigment members without the equivalent association names" do
56
+
57
+ # we should remove has-many & belongs_to from basic assignment set as they require a DB lookup
58
+ # or a Model.create call, not a simple assignment
59
+
60
+ MethodDictionary.assignments_for(Project).should_not include( MethodDictionary.belongs_to_for(Project) )
61
+ MethodDictionary.assignments_for(Project).should_not include( MethodDictionary.has_many_for(Project) )
62
+ end
63
+
64
+
65
+ it "should populate assignment operators for method details for different forms of a column name" do
66
+
67
+ MethodDictionary.build_method_details( Project )
68
+
69
+ [:value_as_string, 'value_as_string', "VALUE as_STRING", "value as string"].each do |format|
70
+
71
+ method_details = MethodDictionary.find_method_detail( Project, format )
72
+
73
+ method_details.class.should == MethodDetail
74
+
75
+ method_details.operator.should == 'value_as_string'
76
+ method_details.operator_for(:assignment).should == 'value_as_string'
77
+
78
+ method_details.operator?('value_as_string').should be_true
79
+ method_details.operator?('blah_as_string').should be_false
80
+
81
+ method_details.operator_for(:belongs_to).should be_nil
82
+ method_details.operator_for(:has_many).should be_nil
83
+ end
84
+ end
85
+
86
+
87
+ # Note : Not all assignments will currently have a column type, for example
88
+ # those that are derived from a delegate_belongs_to
89
+
90
+ it "should populate column types for assignment operators in method details" do
91
+
92
+ MethodDictionary.build_method_details( Project )
93
+
94
+ [:value_as_string, 'value_as_string', "VALUE as_STRING", "value as string"].each do |format|
95
+
96
+ method_details = MethodDictionary.find_method_detail( Project, format )
97
+
98
+ method_details.class.should == MethodDetail
99
+
100
+ method_details.col_type.should_not be_nil
101
+ method_details.col_type.name.should == 'value_as_string'
102
+ method_details.col_type.default.should == nil
103
+ method_details.col_type.sql_type.should include 'varchar(255)' # db specific, sqlite
104
+ method_details.col_type.type.should == :string
105
+ end
106
+ end
107
+
108
+ it "should populate required Class for assignment operators based on column type" do
109
+
110
+ MethodDictionary.build_method_details( Project )
111
+
112
+ [:value_as_string, 'value_as_string', "VALUE as_STRING", "value as string"].each do |format|
113
+
114
+ method_details = MethodDictionary.find_method_detail( Project, format )
115
+
116
+ method_details.operator_class_name.should == 'String'
117
+ method_details.operator_class.should be_is_a(Class)
118
+ method_details.operator_class.should == String
119
+ end
120
+
121
+ end
122
+
123
+ it "should populate belongs_to operator for method details for different forms of a column name" do
124
+
125
+ MethodDictionary.build_method_details( Project )
126
+ MethodDictionary.build_method_details( Milestone )
127
+
128
+ # milestone.project = project.id
129
+ [:project, 'project', "PROJECT", "prOJECt"].each do |format|
130
+
131
+ method_details = MethodDictionary.find_method_detail( Milestone, format )
132
+
133
+ method_details.should_not be_nil
134
+
135
+ method_details.operator.should == 'project'
136
+ method_details.operator_for(:belongs_to).should == 'project'
137
+
138
+ method_details.operator_for(:assignment).should be_nil
139
+ method_details.operator_for(:has_many).should be_nil
140
+ end
141
+
142
+ end
143
+
144
+ it "should populate required Class for belongs_to operator method details" do
145
+
146
+ MethodDictionary.find_operators( LoaderRelease )
147
+ MethodDictionary.find_operators( LongAndComplexTableLinkedToVersion )
148
+
149
+ MethodDictionary.build_method_details( LoaderRelease )
150
+ MethodDictionary.build_method_details( LongAndComplexTableLinkedToVersion )
151
+
152
+
153
+ # release.project = project.id
154
+ [:project, 'project', "PROJECT", "prOJECt"].each do |format|
155
+
156
+ method_details = MethodDictionary.find_method_detail( LoaderRelease, format )
157
+
158
+ method_details.operator_class_name.should == 'Project'
159
+ method_details.operator_class.should == Project
160
+ end
161
+
162
+
163
+ [:version, "Version", "verSION"].each do |format|
164
+ method_details = MethodDictionary.find_method_detail( LongAndComplexTableLinkedToVersion, format )
165
+
166
+ method_details.operator_type.should == :belongs_to
167
+
168
+ method_details.operator_class_name.should == 'Version'
169
+ method_details.operator_class.should == Version
170
+ end
171
+ end
172
+
173
+ it "should populate required Class for has_one operator method details" do
174
+
175
+ MethodDictionary.find_operators( Version )
176
+ MethodDictionary.build_method_details( Version )
177
+
178
+ # version.long_and_complex_table_linked_to_version = LongAndComplexTableLinkedToVersion.create()
179
+
180
+ [:long_and_complex_table_linked_to_version, 'LongAndComplexTableLinkedToVersion', "Long And Complex_Table_Linked To Version", "Long_And_Complex_Table_Linked_To_Version"].each do |format|
181
+ method_details = MethodDictionary.find_method_detail( Version, format )
182
+
183
+ method_details.should_not be_nil
184
+
185
+ method_details.operator.should == 'long_and_complex_table_linked_to_version'
186
+
187
+ method_details.operator_type.should == :has_one
188
+
189
+ method_details.operator_class_name.should == 'LongAndComplexTableLinkedToVersion'
190
+ method_details.operator_class.should == LongAndComplexTableLinkedToVersion
191
+ end
192
+ end
193
+
194
+
195
+ it "should find has_many operator for method details" do
196
+
197
+ MethodDictionary.build_method_details( Project )
198
+
199
+ [:milestones, "Mile Stones", 'mileSTONES', 'MileStones'].each do |format|
200
+
201
+ method_details = MethodDictionary.find_method_detail( Project, format )
202
+
203
+ method_details.class.should == MethodDetail
204
+
205
+ result = 'milestones'
206
+ method_details.operator.should == result
207
+ method_details.operator_for(:has_many).should == result
208
+
209
+ method_details.operator_for(:belongs_to).should be_nil
210
+ method_details.operator_for(:assignments).should be_nil
211
+ end
212
+
213
+ end
214
+
215
+
216
+ it "should return nil when non existent column name" do
217
+ ["On sale", 'on_sale'].each do |format|
218
+ detail = MethodDictionary.find_method_detail( Project, format )
219
+
220
+ detail.should be_nil
221
+ end
222
+ end
223
+
224
+
225
+ it "should find a set of methods based on a list of column names" do
226
+ pending("key API - map column headers to set of methods")
227
+ end
228
+
229
+ it "should not by default map setter methods", :fail => true do
230
+ MethodDictionary.assignments[Milestone].should_not include('title')
231
+ end
232
+
233
+ it "should support reload and inclusion of setter methods", :fail => true do
234
+
235
+ MethodDictionary.assignments[Milestone].should_not include('title')
236
+
237
+ MethodDictionary.find_operators( Milestone, :reload => true, :instance_methods => true )
238
+
239
+ # Milestone delegates :title to Project
240
+ MethodDictionary.assignments[Milestone].should include('title')
241
+ end
242
+
243
+ end