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.
- data/ChangeLog +131 -0
- data/README +156 -0
- data/Rakefile +99 -0
- data/config/database.yml +7 -0
- data/config/database.yml.template +7 -0
- data/config/mysql.schema +72 -0
- data/config/postgresql.schema +39 -0
- data/db/migrate/generic_schema.rb +87 -0
- data/db/migrate/mysql_schema.rb +31 -0
- data/db/migrate/oracle_schema.rb +5 -0
- data/db/migrate/version.rb +4 -0
- data/init.rb +29 -0
- data/lib/ar-extensions.rb +5 -0
- data/lib/ar-extensions/adapters/abstract_adapter.rb +146 -0
- data/lib/ar-extensions/adapters/jdbcmysql.rb +10 -0
- data/lib/ar-extensions/adapters/mysql.rb +10 -0
- data/lib/ar-extensions/adapters/oracle.rb +14 -0
- data/lib/ar-extensions/adapters/postgresql.rb +9 -0
- data/lib/ar-extensions/adapters/sqlite.rb +7 -0
- data/lib/ar-extensions/csv.rb +309 -0
- data/lib/ar-extensions/extensions.rb +506 -0
- data/lib/ar-extensions/finder_options.rb +208 -0
- data/lib/ar-extensions/finder_options/mysql.rb +6 -0
- data/lib/ar-extensions/finders.rb +95 -0
- data/lib/ar-extensions/foreign_keys.rb +70 -0
- data/lib/ar-extensions/fulltext.rb +62 -0
- data/lib/ar-extensions/fulltext/mysql.rb +44 -0
- data/lib/ar-extensions/import.rb +348 -0
- data/lib/ar-extensions/import/jdbcmysql.rb +5 -0
- data/lib/ar-extensions/import/mysql.rb +5 -0
- data/lib/ar-extensions/import/mysql_generic.rb +41 -0
- data/lib/ar-extensions/import/postgresql.rb +0 -0
- data/lib/ar-extensions/import/sqlite.rb +22 -0
- data/lib/ar-extensions/insert_select.rb +184 -0
- data/lib/ar-extensions/insert_select/mysql.rb +6 -0
- data/lib/ar-extensions/synchronize.rb +30 -0
- data/lib/ar-extensions/temporary_table.rb +124 -0
- data/lib/ar-extensions/temporary_table/mysql.rb +3 -0
- data/lib/ar-extensions/util/sql_generation.rb +27 -0
- data/lib/ar-extensions/util/support_methods.rb +43 -0
- data/lib/ar-extensions/version.rb +9 -0
- 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
|
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,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,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
|