wunderbread-ar-extensions 0.8.3

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 (42) hide show
  1. data/ChangeLog +131 -0
  2. data/README +156 -0
  3. data/Rakefile +99 -0
  4. data/config/database.yml +7 -0
  5. data/config/database.yml.template +7 -0
  6. data/config/mysql.schema +72 -0
  7. data/config/postgresql.schema +39 -0
  8. data/db/migrate/generic_schema.rb +87 -0
  9. data/db/migrate/mysql_schema.rb +31 -0
  10. data/db/migrate/oracle_schema.rb +5 -0
  11. data/db/migrate/version.rb +4 -0
  12. data/init.rb +29 -0
  13. data/lib/ar-extensions.rb +5 -0
  14. data/lib/ar-extensions/adapters/abstract_adapter.rb +146 -0
  15. data/lib/ar-extensions/adapters/jdbcmysql.rb +10 -0
  16. data/lib/ar-extensions/adapters/mysql.rb +10 -0
  17. data/lib/ar-extensions/adapters/oracle.rb +14 -0
  18. data/lib/ar-extensions/adapters/postgresql.rb +9 -0
  19. data/lib/ar-extensions/adapters/sqlite.rb +7 -0
  20. data/lib/ar-extensions/csv.rb +309 -0
  21. data/lib/ar-extensions/extensions.rb +506 -0
  22. data/lib/ar-extensions/finder_options.rb +208 -0
  23. data/lib/ar-extensions/finder_options/mysql.rb +6 -0
  24. data/lib/ar-extensions/finders.rb +95 -0
  25. data/lib/ar-extensions/foreign_keys.rb +70 -0
  26. data/lib/ar-extensions/fulltext.rb +62 -0
  27. data/lib/ar-extensions/fulltext/mysql.rb +44 -0
  28. data/lib/ar-extensions/import.rb +348 -0
  29. data/lib/ar-extensions/import/jdbcmysql.rb +5 -0
  30. data/lib/ar-extensions/import/mysql.rb +5 -0
  31. data/lib/ar-extensions/import/mysql_generic.rb +41 -0
  32. data/lib/ar-extensions/import/postgresql.rb +0 -0
  33. data/lib/ar-extensions/import/sqlite.rb +22 -0
  34. data/lib/ar-extensions/insert_select.rb +184 -0
  35. data/lib/ar-extensions/insert_select/mysql.rb +6 -0
  36. data/lib/ar-extensions/synchronize.rb +30 -0
  37. data/lib/ar-extensions/temporary_table.rb +124 -0
  38. data/lib/ar-extensions/temporary_table/mysql.rb +3 -0
  39. data/lib/ar-extensions/util/sql_generation.rb +27 -0
  40. data/lib/ar-extensions/util/support_methods.rb +43 -0
  41. data/lib/ar-extensions/version.rb +9 -0
  42. metadata +107 -0
@@ -0,0 +1,39 @@
1
+ CREATE TABLE topics (
2
+ id serial NOT NULL,
3
+ title character varying(255) default NULL,
4
+ author_name character varying(255) default NULL,
5
+ author_email_address character varying(255) default NULL,
6
+ written_on timestamp default NULL,
7
+ bonus_time time default NULL,
8
+ last_read date default NULL,
9
+ content text,
10
+ approved bool default TRUE,
11
+ replies_count integer default 0,
12
+ parent_id serial default NULL,
13
+ type character varying(50) default NULL,
14
+ PRIMARY KEY (id)
15
+ );
16
+
17
+ CREATE TABLE projects (
18
+ id serial NOT NULL,
19
+ name character varying(100) default NULL,
20
+ type character varying(255) NOT NULL,
21
+ PRIMARY KEY (id)
22
+ );
23
+
24
+ CREATE TABLE developers (
25
+ id serial NOT NULL,
26
+ name character varying(100) default NULL,
27
+ salary integer default 70000,
28
+ created_at timestamp default NULL,
29
+ updated_at timestamp default NULL,
30
+ PRIMARY KEY (id)
31
+ );
32
+
33
+ CREATE TABLE books (
34
+ id serial NOT NULL,
35
+ title character varying(255) NOT NULL,
36
+ publisher character varying(255) NOT NULL,
37
+ author_name character varying(255) NOT NULL,
38
+ PRIMARY KEY (id)
39
+ );
@@ -0,0 +1,87 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ create_table :schema_info, :force=>true do |t|
4
+ t.column :version, :integer, :unique=>true
5
+ end
6
+ SchemaInfo.create :version=>SchemaInfo::VERSION
7
+
8
+ create_table :group, :force => true do |t|
9
+ t.column :order, :string
10
+ t.timestamps
11
+ end
12
+
13
+ create_table :topics, :force=>true do |t|
14
+ t.column :title, :string, :null=>false
15
+ t.column :author_name, :string
16
+ t.column :author_email_address, :string
17
+ t.column :written_on, :datetime
18
+ t.column :bonus_time, :time
19
+ t.column :last_read, :datetime
20
+ t.column :content, :text
21
+ t.column :approved, :boolean, :default=>'1'
22
+ t.column :replies_count, :integer
23
+ t.column :parent_id, :integer
24
+ t.column :type, :string
25
+ t.column :created_at, :datetime
26
+ t.column :updated_at, :datetime
27
+ end
28
+
29
+ create_table :projects, :force=>true do |t|
30
+ t.column :name, :string
31
+ t.column :type, :string
32
+ end
33
+
34
+ create_table :developers, :force=>true do |t|
35
+ t.column :name, :string
36
+ t.column :salary, :integer, :default=>'70000'
37
+ t.column :created_at, :datetime
38
+ t.column :team_id, :integer
39
+ t.column :updated_at, :datetime
40
+ end
41
+
42
+ create_table :addresses, :force=>true do |t|
43
+ t.column :address, :string
44
+ t.column :city, :string
45
+ t.column :state, :string
46
+ t.column :zip, :string
47
+ t.column :developer_id, :integer
48
+ end
49
+
50
+ create_table :teams, :force=>true do |t|
51
+ t.column :name, :string
52
+ end
53
+
54
+ create_table :books, :force=>true do |t|
55
+ t.column :title, :string, :null=>false
56
+ t.column :publisher, :string, :null=>false
57
+ t.column :author_name, :string, :null=>false
58
+ t.column :created_at, :datetime
59
+ t.column :created_on, :datetime
60
+ t.column :updated_at, :datetime
61
+ t.column :updated_on, :datetime
62
+ t.column :topic_id, :integer
63
+ t.column :for_sale, :boolean, :default => true
64
+ end
65
+
66
+ create_table :languages, :force=>true do |t|
67
+ t.column :name, :string
68
+ t.column :developer_id, :integer
69
+ end
70
+
71
+ create_table :shopping_carts, :force=>true do |t|
72
+ t.column :name, :string, :null => true
73
+ t.column :created_at, :datetime
74
+ t.column :updated_at, :datetime
75
+ end
76
+
77
+ create_table :cart_items, :force => true do |t|
78
+ t.column :shopping_cart_id, :string, :null => false
79
+ t.column :book_id, :string, :null => false
80
+ t.column :copies, :integer, :default => 1
81
+ t.column :created_at, :datetime
82
+ t.column :updated_at, :datetime
83
+ end
84
+
85
+ add_index :cart_items, [:shopping_cart_id, :book_id], :unique => true, :name => 'uk_shopping_cart_books'
86
+
87
+ end
@@ -0,0 +1,31 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ create_table :test_myisam, :options=>'ENGINE=MyISAM', :force=>true do |t|
4
+ t.column :my_name, :string, :null=>false
5
+ t.column :description, :string
6
+ end
7
+
8
+ create_table :test_innodb, :options=>'ENGINE=InnoDb', :force=>true do |t|
9
+ t.column :my_name, :string, :null=>false
10
+ t.column :description, :string
11
+ end
12
+
13
+ create_table :test_memory, :options=>'ENGINE=Memory', :force=>true do |t|
14
+ t.column :my_name, :string, :null=>false
15
+ t.column :description, :string
16
+ end
17
+
18
+ create_table :books, :options=>'ENGINE=MyISAM', :force=>true do |t|
19
+ t.column :title, :string, :null=>false
20
+ t.column :publisher, :string, :null=>false
21
+ t.column :author_name, :string, :null=>false
22
+ t.column :created_at, :datetime
23
+ t.column :created_on, :datetime
24
+ t.column :updated_at, :datetime
25
+ t.column :updated_on, :datetime
26
+ t.column :topic_id, :integer
27
+ t.column :for_sale, :boolean, :default => true
28
+ end
29
+ execute "ALTER TABLE books ADD FULLTEXT( `title`, `publisher`, `author_name` )"
30
+
31
+ end
@@ -0,0 +1,5 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ execute "CREATE SEQUENCE books_seq START_WITH 1"
4
+
5
+ end
@@ -0,0 +1,4 @@
1
+ class SchemaInfo < ActiveRecord::Base
2
+ set_table_name 'schema_info'
3
+ VERSION = 10
4
+ end
data/init.rb ADDED
@@ -0,0 +1,29 @@
1
+
2
+ require 'ostruct'
3
+ begin ; require 'active_record' ; rescue LoadError; require 'rubygems'; require 'active_record'; end
4
+
5
+ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), 'lib'))
6
+
7
+ require 'ar-extensions/util/sql_generation'
8
+ require 'ar-extensions/util/support_methods'
9
+ require 'ar-extensions/version'
10
+ require 'ar-extensions/extensions'
11
+ require 'ar-extensions/finder_options'
12
+ require 'ar-extensions/foreign_keys'
13
+ require 'ar-extensions/fulltext'
14
+ require 'ar-extensions/import'
15
+ require 'ar-extensions/insert_select'
16
+ require 'ar-extensions/finders'
17
+ require 'ar-extensions/synchronize'
18
+ require 'ar-extensions/temporary_table'
19
+ require 'ar-extensions/adapters/abstract_adapter'
20
+
21
+ #load all available functionality for specified adapter
22
+ # Ex. ENV['LOAD_ADAPTER_EXTENSIONS'] = 'mysql'
23
+ if ENV['LOAD_ADAPTER_EXTENSIONS']
24
+ require "active_record/connection_adapters/#{ENV['LOAD_ADAPTER_EXTENSIONS']}_adapter.rb"
25
+ file_regexp = File.join(File.dirname(__FILE__), 'lib', 'ar-extensions','**',
26
+ "#{ENV['LOAD_ADAPTER_EXTENSIONS']}.rb")
27
+
28
+ Dir.glob(file_regexp){|file| require(file) }
29
+ end
@@ -0,0 +1,5 @@
1
+ begin ; require 'rubygems' rescue LoadError; end
2
+ require 'active_record' # ActiveRecord loads the Benchmark library automatically
3
+ require 'active_record/version'
4
+
5
+ require File.expand_path(File.join( File.dirname( __FILE__ ), '..', 'init.rb' ))
@@ -0,0 +1,146 @@
1
+ module ActiveRecord # :nodoc:
2
+ module ConnectionAdapters # :nodoc:
3
+ class AbstractAdapter # :nodoc:
4
+ NO_MAX_PACKET = 0
5
+ QUERY_OVERHEAD = 8 #This was shown to be true for MySQL, but it's not clear where the overhead is from.
6
+
7
+ def next_value_for_sequence(sequence_name)
8
+ %{#{sequence_name}.nextval}
9
+ end
10
+
11
+ # +sql+ can be a single string or an array. If it is an array all
12
+ # elements that are in position >= 1 will be appended to the final SQL.
13
+ def insert_many( sql, values, *args ) # :nodoc:
14
+ # the number of inserts default
15
+ number_of_inserts = 0
16
+
17
+ base_sql,post_sql = if sql.is_a?( String )
18
+ [ sql, '' ]
19
+ elsif sql.is_a?( Array )
20
+ [ sql.shift, sql.join( ' ' ) ]
21
+ end
22
+
23
+ sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size
24
+
25
+ # the number of bytes the requested insert statement values will take up
26
+ values_in_bytes = self.class.sum_sizes( *values )
27
+
28
+ # the number of bytes (commas) it will take to comma separate our values
29
+ comma_separated_bytes = values.size-1
30
+
31
+ # the total number of bytes required if this statement is one statement
32
+ total_bytes = sql_size + values_in_bytes + comma_separated_bytes
33
+
34
+ max = max_allowed_packet
35
+
36
+ # if we can insert it all as one statement
37
+ if NO_MAX_PACKET == max or total_bytes < max
38
+ number_of_inserts += 1
39
+ sql2insert = base_sql + values.join( ',' ) + post_sql
40
+ insert( sql2insert, *args )
41
+ else
42
+ value_sets = self.class.get_insert_value_sets( values, sql_size, max )
43
+ value_sets.each do |values|
44
+ number_of_inserts += 1
45
+ sql2insert = base_sql + values.join( ',' ) + post_sql
46
+ insert( sql2insert, *args )
47
+ end
48
+ end
49
+
50
+ number_of_inserts
51
+ end
52
+
53
+ def pre_sql_statements(options)
54
+ sql = []
55
+ sql << options[:pre_sql] if options[:pre_sql]
56
+ sql << options[:command] if options[:command]
57
+ sql << "IGNORE" if options[:ignore]
58
+
59
+ #add keywords like IGNORE or DELAYED
60
+ if options[:keywords].is_a?(Array)
61
+ sql.concat(options[:keywords])
62
+ elsif options[:keywords]
63
+ sql << options[:keywords].to_s
64
+ end
65
+
66
+ sql
67
+ end
68
+
69
+ # Synchronizes the passed in ActiveRecord instances with the records in
70
+ # the database by calling +reload+ on each instance.
71
+ def after_import_synchronize( instances )
72
+ instances.each { |e| e.reload }
73
+ end
74
+
75
+ # Returns an array of post SQL statements given the passed in options.
76
+ def post_sql_statements( table_name, options ) # :nodoc:
77
+ post_sql_statements = []
78
+ if options[:on_duplicate_key_update]
79
+ post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
80
+ end
81
+
82
+ #custom user post_sql
83
+ post_sql_statements << options[:post_sql] if options[:post_sql]
84
+
85
+ #with rollup
86
+ post_sql_statements << rollup_sql if options[:rollup]
87
+
88
+ post_sql_statements
89
+ end
90
+
91
+
92
+ # Generates the INSERT statement used in insert multiple value sets.
93
+ def multiple_value_sets_insert_sql(table_name, column_names, options) # :nodoc:
94
+ "INSERT #{options[:ignore] ? 'IGNORE ':''}INTO #{table_name} (#{column_names.join(',')}) VALUES "
95
+ end
96
+
97
+ # Returns SQL the VALUES for an INSERT statement given the passed in +columns+
98
+ # and +array_of_attributes+.
99
+ def values_sql_for_column_names_and_attributes( columns, array_of_attributes ) # :nodoc:
100
+ values = []
101
+ array_of_attributes.each do |arr|
102
+ my_values = []
103
+ arr.each_with_index do |val,j|
104
+ my_values << quote( val, columns[j] )
105
+ end
106
+ values << my_values
107
+ end
108
+ values_arr = values.map{ |arr| '(' + arr.join( ',' ) + ')' }
109
+ end
110
+
111
+ # Returns the sum of the sizes of the passed in objects. This should
112
+ # probably be moved outside this class, but to where?
113
+ def self.sum_sizes( *objects ) # :nodoc:
114
+ objects.inject( 0 ){|sum,o| sum += o.size }
115
+ end
116
+
117
+ # Returns the maximum number of bytes that the server will allow
118
+ # in a single packet
119
+ def max_allowed_packet
120
+ NO_MAX_PACKET
121
+ end
122
+
123
+ def self.get_insert_value_sets( values, sql_size, max_bytes ) # :nodoc:
124
+ value_sets = []
125
+ arr, current_arr_values_size, current_size = [], 0, 0
126
+ values.each_with_index do |val,i|
127
+ comma_bytes = arr.size
128
+ sql_size_thus_far = sql_size + current_size + val.size + comma_bytes
129
+ if NO_MAX_PACKET == max_bytes or sql_size_thus_far <= max_bytes
130
+ current_size += val.size
131
+ arr << val
132
+ else
133
+ value_sets << arr
134
+ arr = [ val ]
135
+ current_size = val.size
136
+ end
137
+
138
+ # if we're on the last iteration push whatever we have in arr to value_sets
139
+ value_sets << arr if i == (values.size-1)
140
+ end
141
+ [ *value_sets ]
142
+ end
143
+
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,10 @@
1
+ ActiveRecord::ConnectionAdapters::JdbcAdapter.class_eval do
2
+ # Returns the maximum number of bytes that the server will allow
3
+ # in a single packet
4
+ def max_allowed_packet # :nodoc:
5
+ result = execute( "SHOW VARIABLES like 'max_allowed_packet';" )
6
+ result[0]["Value"].to_i
7
+ end
8
+
9
+ def rollup_sql; " WITH ROLLUP "; end
10
+ end
@@ -0,0 +1,10 @@
1
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
2
+ # Returns the maximum number of bytes that the server will allow
3
+ # in a single packet
4
+ def max_allowed_packet # :nodoc:
5
+ result = execute( "SHOW VARIABLES like 'max_allowed_packet';" )
6
+ result.fetch_row[1].to_i
7
+ end
8
+
9
+ def rollup_sql; " WITH ROLLUP "; end
10
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveRecord # :nodoc:
2
+ module ConnectionAdapters # :nodoc:
3
+ class OracleAdapter # :nodoc:
4
+
5
+ def next_value_for_sequence(sequence_name)
6
+ %{#{sequence_name}.nextval}
7
+ end
8
+
9
+ def supports_import?
10
+ true
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveRecord # :nodoc:
2
+ module ConnectionAdapters # :nodoc:
3
+ class PostgreSQLAdapter # :nodoc:
4
+ def next_value_for_sequence(sequence_name)
5
+ %{nextval('#{sequence_name}')}
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveRecord # :nodoc:
2
+ module ConnectionAdapters # :nodoc:
3
+ class SqliteAdapter # :nodoc:
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,309 @@
1
+ begin
2
+ require 'faster_csv'
3
+ require 'ar-extensions/csv'
4
+ rescue LoadError => ex
5
+ STDERR.puts "FasterCSV is not installed. CSV functionality will not be included."
6
+ raise ex
7
+ end
8
+
9
+
10
+ # Adds CSV export options to ActiveRecord::Base models.
11
+ #
12
+ # === Example 1, exporting all fields
13
+ # class Book < ActiveRecord::Base ; end
14
+ #
15
+ # book = Book.find( 1 )
16
+ # book.to_csv
17
+ #
18
+ # === Example 2, only exporting certain fields
19
+ # class Book < ActiveRecord::Base ; end
20
+ #
21
+ # book = Book.find( 1 )
22
+ # book.to_csv( :only=>%W( title isbn )
23
+ #
24
+ # === Example 3, exporting a model including a belongs_to association
25
+ # class Book < ActiveRecord::Base
26
+ # belongs_to :author
27
+ # end
28
+ #
29
+ # book = Book.find( 1 )
30
+ # book.to_csv( :include=>:author )
31
+ #
32
+ # This also works for a has_one relationship. The :include
33
+ # option can also be an array of has_one/belongs_to
34
+ # associations. This by default includes all fields
35
+ # on the belongs_to association.
36
+ #
37
+ # === Example 4, exporting a model including a has_many association
38
+ # class Book < ActiveRecord::Base
39
+ # has_many :tags
40
+ # end
41
+ #
42
+ # book = Book.find( 1 )
43
+ # book.to_csv( :include=>:tags )
44
+ #
45
+ # This by default includes all fields on the has_many assocaition.
46
+ # This can also be an array of multiple has_many relationships. The
47
+ # array can be mixed with has_one/belongs_to associations array
48
+ # as well. IE: :include=>[ :author, :sales ]
49
+ #
50
+ # === Example 5, nesting associations
51
+ # class Book < ActiveRecord::Base
52
+ # belongs_to :author
53
+ # has_many :tags
54
+ # end
55
+ #
56
+ # book = Book.find( 1 )
57
+ # book.to_csv( :includes=>{
58
+ # :author => { :only=>%W( name ) },
59
+ # :tags => { :only=>%W( tagname ) } )
60
+ #
61
+ # Each included association can receive an options Hash. This
62
+ # allows you to nest the associations as deep as you want
63
+ # for your CSV export.
64
+ #
65
+ # It is not recommended to nest multiple has_many associations,
66
+ # although nesting multiple has_one/belongs_to associations.
67
+ #
68
+ module ActiveRecord::Extensions::FindToCSV
69
+
70
+ def self.included(base)
71
+ if !base.respond_to?(:find_with_csv)
72
+ base.class_eval do
73
+ extend ClassMethods
74
+ include InstanceMethods
75
+ end
76
+ class << base
77
+ alias_method_chain :find, :csv
78
+ end
79
+ end
80
+ end
81
+
82
+ class FieldMap# :nodoc:
83
+ attr_reader :fields, :fields_to_headers
84
+
85
+ def initialize( fields, fields_to_headers ) # :nodoc:
86
+ @fields, @fields_to_headers = fields, fields_to_headers
87
+ end
88
+
89
+ def headers # :nodoc:
90
+ @headers ||= fields.inject( [] ){ |arr,field| arr << fields_to_headers[ field ] }
91
+ end
92
+
93
+ end
94
+
95
+ module ClassMethods # :nodoc:
96
+ private
97
+
98
+ def to_csv_fields_for_nil # :nodoc:
99
+ self.columns.map{ |column| column.name }.sort
100
+ end
101
+
102
+ def to_csv_headers_for_included_associations( includes ) # :nodoc:
103
+ get_class = proc { |str| Object.const_get( self.reflections[ str.to_sym ].class_name ) }
104
+
105
+ case includes
106
+ when Symbol
107
+ [ get_class.call( includes ).to_csv_headers( :headers=>true, :naming=>":model[:header]" ) ]
108
+ when Array
109
+ includes.map do |association|
110
+ clazz = get_class.call( association )
111
+ clazz.to_csv_headers( :headers=>true, :naming=>":model[:header]" )
112
+ end
113
+ when Hash
114
+ includes.sort_by{ |k| k.to_s }.inject( [] ) do |arr,(association,options)|
115
+ clazz = get_class.call( association )
116
+ if options[:headers].is_a?( Hash )
117
+ options.merge!( :naming=>":header" )
118
+ else
119
+ options.merge!( :naming=>":model[:header]" )
120
+ end
121
+ arr << clazz.to_csv_headers( options )
122
+ end
123
+ else
124
+ []
125
+ end
126
+ end
127
+
128
+ public
129
+
130
+ def find_with_csv( *args ) # :nodoc:
131
+ results = find_without_csv( *args )
132
+ results.extend( ArrayInstanceMethods ) if results.is_a?( Array )
133
+ results
134
+ end
135
+
136
+ def to_csv_fields( options={} ) # :nodoc:
137
+ fields_to_headers, fields = {}, []
138
+
139
+ headers = options[:headers]
140
+ case headers
141
+ when Array
142
+ fields = headers.map{ |e| e.to_s }
143
+ when Hash
144
+ headers = headers.inject( {} ){ |hsh,(k,v)| hsh[k.to_s] = v ; hsh }
145
+ fields = headers.keys.sort
146
+ fields.each { |field| fields_to_headers[field] = headers[field] }
147
+ else
148
+ fields = to_csv_fields_for_nil
149
+ end
150
+
151
+ if options[:only]
152
+ specified_fields = options[:only].map{ |e| e.to_s }
153
+ fields.delete_if{ |field| not specified_fields.include?( field ) }
154
+ elsif options[:except]
155
+ excluded_fields = options[:except].map{ |e| e.to_s }
156
+ fields.delete_if{ |field| excluded_fields.include?( field ) }
157
+ end
158
+
159
+ fields.each{ |field| fields_to_headers[field] = field } if fields_to_headers.empty?
160
+
161
+ FieldMap.new( fields, fields_to_headers )
162
+ end
163
+
164
+ # Returns an array of CSV headers passed in the array of +options+.
165
+ def to_csv_headers( options={} )
166
+ options = { :headers=>true, :naming=>":header" }.merge( options )
167
+ return nil if not options[:headers]
168
+
169
+ fieldmap = to_csv_fields( options )
170
+ headers = fieldmap.headers
171
+ headers.push( *to_csv_headers_for_included_associations( options[ :include ] ).flatten )
172
+ headers.map{ |header| options[:naming].gsub( /:header/, header ).gsub( /:model/, self.name.downcase ) }
173
+ end
174
+
175
+ end
176
+
177
+
178
+ module InstanceMethods
179
+
180
+ private
181
+
182
+ def to_csv_data_for_included_associations( includes ) # :nodoc:
183
+ get_class = proc { |str| Object.const_get( self.class.reflections[ str.to_sym ].class_name ) }
184
+
185
+ case includes
186
+ when Symbol
187
+ association = self.send( includes )
188
+ association.send( :extend, ArrayInstanceMethods ) if association.is_a?( Array )
189
+ if association.nil? or (association.respond_to?( :empty? ) and association.empty?)
190
+ [ get_class.call( includes ).columns.map{ '' } ]
191
+ else
192
+ [ *association.to_csv_data ]
193
+ end
194
+ when Array
195
+ siblings = []
196
+ includes.each do |association_name|
197
+ association = self.send( association_name )
198
+ association.send( :extend, ArrayInstanceMethods ) if association.is_a?( Array )
199
+ if association.nil? or (association.respond_to?( :empty? ) and association.empty?)
200
+ association_data = [ get_class.call( association_name ).columns.map{ '' } ]
201
+ else
202
+ association_data = association.to_csv_data
203
+ end
204
+
205
+ if siblings.empty?
206
+ siblings.push( *association_data )
207
+ else
208
+ temp = []
209
+ association_data.each do |assoc_csv|
210
+ siblings.each do |sibling|
211
+ temp.push( sibling + assoc_csv )
212
+ end
213
+ end
214
+ siblings = temp
215
+ end
216
+ end
217
+ siblings
218
+ when Hash
219
+ sorted_includes = includes.sort_by{ |k| k.to_s }
220
+ siblings = []
221
+ sorted_includes.each do |(association_name,options)|
222
+ association = self.send( association_name )
223
+ association.send( :extend, ArrayInstanceMethods ) if association.is_a?( Array )
224
+ if association.nil? or (association.respond_to?( :empty ) and association.empty?)
225
+ association_data = [ get_class.call( association_name ).columns.map{ '' } ]
226
+ else
227
+ association_data = association.to_csv_data( options )
228
+ end
229
+
230
+ if siblings.empty?
231
+ siblings.push( *association_data )
232
+ else
233
+ temp = []
234
+ association_data.each do |assoc_csv|
235
+ siblings.each do |sibling|
236
+ temp.push( sibling + assoc_csv )
237
+ end
238
+ end
239
+ siblings = temp
240
+ end
241
+ end
242
+ siblings
243
+ else
244
+ []
245
+ end
246
+ end
247
+
248
+ public
249
+
250
+ # Returns CSV data without any header rows for the passed in +options+.
251
+ def to_csv_data( options={} )
252
+ fields = self.class.to_csv_fields( options ).fields
253
+ data, model_data = [], fields.inject( [] ) { |arr,field| arr << attributes[field].to_s }
254
+ if options[:include]
255
+ to_csv_data_for_included_associations( options[:include ] ).map do |assoc_csv_data|
256
+ data << model_data + assoc_csv_data
257
+ end
258
+ else
259
+ data << model_data
260
+ end
261
+ data
262
+ end
263
+
264
+ # Returns CSV data including header rows for the passed in +options+.
265
+ def to_csv( options={} )
266
+ FasterCSV.generate do |csv|
267
+ headers = self.class.to_csv_headers( options )
268
+ csv << headers if headers
269
+ to_csv_data( options ).each{ |data| csv << data }
270
+ end
271
+ end
272
+
273
+ end
274
+
275
+ module ArrayInstanceMethods # :nodoc:
276
+ class NoRecordsError < StandardError ; end #:nodoc:
277
+
278
+ # Returns CSV headers for an array of ActiveRecord::Base
279
+ # model objects by calling to_csv_headers on the first
280
+ # element.
281
+ def to_csv_headers( options={} )
282
+ first.class.to_csv_headers( options )
283
+ end
284
+
285
+ # Returns CSV data without headers for an array of
286
+ # ActiveRecord::Base model objects by iterating over them and
287
+ # calling to_csv_data with the passed in +options+.
288
+ def to_csv_data( options={} )
289
+ inject( [] ) do |arr,model_instance|
290
+ arr.push( *model_instance.to_csv_data( options ) )
291
+ end
292
+ end
293
+
294
+ # Returns CSV data with headers for an array of ActiveRecord::Base
295
+ # model objects by iterating over them and calling to_csv with
296
+ # the passed in +options+.
297
+ def to_csv( options={} )
298
+ FasterCSV.generate do |csv|
299
+ headers = to_csv_headers( options )
300
+ csv << headers if headers
301
+ each do |model_instance|
302
+ model_instance.to_csv_data( options ).each{ |data| csv << data }
303
+ end
304
+ end
305
+ end
306
+
307
+ end
308
+
309
+ end