activerecord 1.15.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (185) hide show
  1. data/CHANGELOG +2454 -34
  2. data/README +1 -1
  3. data/RUNNING_UNIT_TESTS +3 -34
  4. data/Rakefile +98 -77
  5. data/install.rb +1 -1
  6. data/lib/active_record.rb +13 -22
  7. data/lib/active_record/aggregations.rb +38 -49
  8. data/lib/active_record/associations.rb +452 -333
  9. data/lib/active_record/associations/association_collection.rb +66 -20
  10. data/lib/active_record/associations/association_proxy.rb +9 -8
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +46 -51
  12. data/lib/active_record/associations/has_many_association.rb +21 -57
  13. data/lib/active_record/associations/has_many_through_association.rb +38 -18
  14. data/lib/active_record/associations/has_one_association.rb +30 -14
  15. data/lib/active_record/attribute_methods.rb +253 -0
  16. data/lib/active_record/base.rb +719 -494
  17. data/lib/active_record/calculations.rb +62 -63
  18. data/lib/active_record/callbacks.rb +57 -83
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +38 -9
  20. data/lib/active_record/connection_adapters/abstract/database_statements.rb +56 -15
  21. data/lib/active_record/connection_adapters/abstract/query_cache.rb +87 -0
  22. data/lib/active_record/connection_adapters/abstract/quoting.rb +23 -12
  23. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +191 -62
  24. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +37 -34
  25. data/lib/active_record/connection_adapters/abstract_adapter.rb +28 -17
  26. data/lib/active_record/connection_adapters/mysql_adapter.rb +119 -37
  27. data/lib/active_record/connection_adapters/postgresql_adapter.rb +473 -210
  28. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
  29. data/lib/active_record/connection_adapters/sqlite_adapter.rb +91 -107
  30. data/lib/active_record/fixtures.rb +503 -113
  31. data/lib/active_record/locking/optimistic.rb +72 -34
  32. data/lib/active_record/migration.rb +80 -57
  33. data/lib/active_record/observer.rb +13 -10
  34. data/lib/active_record/query_cache.rb +16 -57
  35. data/lib/active_record/reflection.rb +35 -38
  36. data/lib/active_record/schema.rb +5 -5
  37. data/lib/active_record/schema_dumper.rb +35 -13
  38. data/lib/active_record/serialization.rb +98 -0
  39. data/lib/active_record/serializers/json_serializer.rb +71 -0
  40. data/lib/active_record/{xml_serialization.rb → serializers/xml_serializer.rb} +90 -83
  41. data/lib/active_record/timestamp.rb +20 -21
  42. data/lib/active_record/transactions.rb +39 -43
  43. data/lib/active_record/validations.rb +256 -107
  44. data/lib/active_record/version.rb +3 -3
  45. data/lib/activerecord.rb +1 -0
  46. data/test/aaa_create_tables_test.rb +15 -2
  47. data/test/abstract_unit.rb +24 -17
  48. data/test/active_schema_test_mysql.rb +20 -8
  49. data/test/adapter_test.rb +23 -5
  50. data/test/adapter_test_sqlserver.rb +15 -1
  51. data/test/aggregations_test.rb +16 -1
  52. data/test/all.sh +2 -2
  53. data/test/associations/ar_joins_test.rb +0 -0
  54. data/test/associations/callbacks_test.rb +51 -30
  55. data/test/associations/cascaded_eager_loading_test.rb +1 -29
  56. data/test/associations/eager_singularization_test.rb +145 -0
  57. data/test/associations/eager_test.rb +42 -6
  58. data/test/associations/extension_test.rb +6 -1
  59. data/test/associations/inner_join_association_test.rb +88 -0
  60. data/test/associations/join_model_test.rb +47 -16
  61. data/test/associations_test.rb +449 -226
  62. data/test/attribute_methods_test.rb +97 -0
  63. data/test/base_test.rb +251 -105
  64. data/test/binary_test.rb +22 -27
  65. data/test/calculations_test.rb +37 -5
  66. data/test/callbacks_test.rb +23 -0
  67. data/test/connection_test_firebird.rb +2 -2
  68. data/test/connection_test_mysql.rb +30 -0
  69. data/test/connections/native_mysql/connection.rb +3 -0
  70. data/test/connections/native_sqlite/connection.rb +5 -14
  71. data/test/connections/native_sqlite3/connection.rb +5 -14
  72. data/test/connections/native_sqlite3/in_memory_connection.rb +1 -1
  73. data/test/{copy_table_sqlite.rb → copy_table_test_sqlite.rb} +8 -3
  74. data/test/datatype_test_postgresql.rb +178 -27
  75. data/test/{empty_date_time_test.rb → date_time_test.rb} +13 -1
  76. data/test/defaults_test.rb +8 -1
  77. data/test/deprecated_finder_test.rb +7 -128
  78. data/test/finder_test.rb +192 -54
  79. data/test/fixtures/all/developers.yml +0 -0
  80. data/test/fixtures/all/people.csv +0 -0
  81. data/test/fixtures/all/tasks.yml +0 -0
  82. data/test/fixtures/author.rb +12 -5
  83. data/test/fixtures/binaries.yml +130 -435
  84. data/test/fixtures/category.rb +6 -0
  85. data/test/fixtures/company.rb +8 -1
  86. data/test/fixtures/computer.rb +1 -0
  87. data/test/fixtures/contact.rb +16 -0
  88. data/test/fixtures/customer.rb +2 -2
  89. data/test/fixtures/db_definitions/db2.drop.sql +1 -0
  90. data/test/fixtures/db_definitions/db2.sql +4 -0
  91. data/test/fixtures/db_definitions/firebird.drop.sql +3 -1
  92. data/test/fixtures/db_definitions/firebird.sql +6 -0
  93. data/test/fixtures/db_definitions/frontbase.drop.sql +1 -0
  94. data/test/fixtures/db_definitions/frontbase.sql +5 -0
  95. data/test/fixtures/db_definitions/openbase.sql +41 -25
  96. data/test/fixtures/db_definitions/oracle.drop.sql +2 -0
  97. data/test/fixtures/db_definitions/oracle.sql +5 -0
  98. data/test/fixtures/db_definitions/postgresql.drop.sql +7 -0
  99. data/test/fixtures/db_definitions/postgresql.sql +87 -58
  100. data/test/fixtures/db_definitions/postgresql2.sql +1 -2
  101. data/test/fixtures/db_definitions/schema.rb +280 -0
  102. data/test/fixtures/db_definitions/schema2.rb +11 -0
  103. data/test/fixtures/db_definitions/sqlite.drop.sql +1 -0
  104. data/test/fixtures/db_definitions/sqlite.sql +4 -0
  105. data/test/fixtures/db_definitions/sybase.drop.sql +1 -0
  106. data/test/fixtures/db_definitions/sybase.sql +4 -0
  107. data/test/fixtures/developer.rb +10 -0
  108. data/test/fixtures/example.log +1 -0
  109. data/test/fixtures/flowers.jpg +0 -0
  110. data/test/fixtures/item.rb +7 -0
  111. data/test/fixtures/items.yml +4 -0
  112. data/test/fixtures/joke.rb +0 -3
  113. data/test/fixtures/matey.rb +4 -0
  114. data/test/fixtures/mateys.yml +4 -0
  115. data/test/fixtures/minimalistic.rb +2 -0
  116. data/test/fixtures/minimalistics.yml +2 -0
  117. data/test/fixtures/mixins.yml +2 -100
  118. data/test/fixtures/parrot.rb +13 -0
  119. data/test/fixtures/parrots.yml +27 -0
  120. data/test/fixtures/parrots_pirates.yml +7 -0
  121. data/test/fixtures/pirate.rb +5 -0
  122. data/test/fixtures/pirates.yml +9 -0
  123. data/test/fixtures/post.rb +1 -0
  124. data/test/fixtures/project.rb +3 -2
  125. data/test/fixtures/reserved_words/distinct.yml +5 -0
  126. data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
  127. data/test/fixtures/reserved_words/group.yml +14 -0
  128. data/test/fixtures/reserved_words/select.yml +8 -0
  129. data/test/fixtures/reserved_words/values.yml +7 -0
  130. data/test/fixtures/ship.rb +3 -0
  131. data/test/fixtures/ships.yml +5 -0
  132. data/test/fixtures/tagging.rb +4 -0
  133. data/test/fixtures/taggings.yml +8 -1
  134. data/test/fixtures/topic.rb +13 -1
  135. data/test/fixtures/treasure.rb +4 -0
  136. data/test/fixtures/treasures.yml +10 -0
  137. data/test/fixtures_test.rb +205 -24
  138. data/test/inheritance_test.rb +7 -1
  139. data/test/json_serialization_test.rb +180 -0
  140. data/test/lifecycle_test.rb +1 -1
  141. data/test/locking_test.rb +85 -2
  142. data/test/migration_test.rb +206 -40
  143. data/test/mixin_test.rb +13 -515
  144. data/test/pk_test.rb +3 -6
  145. data/test/query_cache_test.rb +104 -0
  146. data/test/reflection_test.rb +16 -0
  147. data/test/reserved_word_test_mysql.rb +177 -0
  148. data/test/schema_dumper_test.rb +38 -3
  149. data/test/serialization_test.rb +47 -0
  150. data/test/transactions_test.rb +74 -23
  151. data/test/unconnected_test.rb +1 -1
  152. data/test/validations_test.rb +322 -32
  153. data/test/xml_serialization_test.rb +121 -44
  154. metadata +48 -41
  155. data/examples/associations.rb +0 -87
  156. data/examples/shared_setup.rb +0 -15
  157. data/examples/validation.rb +0 -85
  158. data/lib/active_record/acts/list.rb +0 -256
  159. data/lib/active_record/acts/nested_set.rb +0 -211
  160. data/lib/active_record/acts/tree.rb +0 -96
  161. data/lib/active_record/connection_adapters/db2_adapter.rb +0 -228
  162. data/lib/active_record/connection_adapters/firebird_adapter.rb +0 -728
  163. data/lib/active_record/connection_adapters/frontbase_adapter.rb +0 -861
  164. data/lib/active_record/connection_adapters/openbase_adapter.rb +0 -350
  165. data/lib/active_record/connection_adapters/oracle_adapter.rb +0 -690
  166. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +0 -591
  167. data/lib/active_record/connection_adapters/sybase_adapter.rb +0 -662
  168. data/lib/active_record/deprecated_associations.rb +0 -104
  169. data/lib/active_record/deprecated_finders.rb +0 -44
  170. data/lib/active_record/vendor/simple.rb +0 -693
  171. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  172. data/lib/active_record/wrappings.rb +0 -58
  173. data/test/connections/native_sqlserver/connection.rb +0 -23
  174. data/test/connections/native_sqlserver_odbc/connection.rb +0 -25
  175. data/test/deprecated_associations_test.rb +0 -396
  176. data/test/fixtures/db_definitions/mysql.drop.sql +0 -32
  177. data/test/fixtures/db_definitions/mysql.sql +0 -234
  178. data/test/fixtures/db_definitions/mysql2.drop.sql +0 -2
  179. data/test/fixtures/db_definitions/mysql2.sql +0 -5
  180. data/test/fixtures/db_definitions/sqlserver.drop.sql +0 -34
  181. data/test/fixtures/db_definitions/sqlserver.sql +0 -243
  182. data/test/fixtures/db_definitions/sqlserver2.drop.sql +0 -2
  183. data/test/fixtures/db_definitions/sqlserver2.sql +0 -5
  184. data/test/fixtures/mixin.rb +0 -63
  185. data/test/mixin_nested_set_test.rb +0 -196
@@ -1,87 +0,0 @@
1
- require File.dirname(__FILE__) + '/shared_setup'
2
-
3
- logger = Logger.new(STDOUT)
4
-
5
- # Database setup ---------------
6
-
7
- logger.info "\nCreate tables"
8
-
9
- [ "DROP TABLE companies", "DROP TABLE people", "DROP TABLE people_companies",
10
- "CREATE TABLE companies (id int(11) auto_increment, client_of int(11), name varchar(255), type varchar(100), PRIMARY KEY (id))",
11
- "CREATE TABLE people (id int(11) auto_increment, name varchar(100), PRIMARY KEY (id))",
12
- "CREATE TABLE people_companies (person_id int(11), company_id int(11), PRIMARY KEY (person_id, company_id))",
13
- ].each { |statement|
14
- # Tables doesn't necessarily already exist
15
- begin; ActiveRecord::Base.connection.execute(statement); rescue ActiveRecord::StatementInvalid; end
16
- }
17
-
18
-
19
- # Class setup ---------------
20
-
21
- class Company < ActiveRecord::Base
22
- has_and_belongs_to_many :people, :class_name => "Person", :join_table => "people_companies", :table_name => "people"
23
- end
24
-
25
- class Firm < Company
26
- has_many :clients, :foreign_key => "client_of"
27
-
28
- def people_with_all_clients
29
- clients.inject([]) { |people, client| people + client.people }
30
- end
31
- end
32
-
33
- class Client < Company
34
- belongs_to :firm, :foreign_key => "client_of"
35
- end
36
-
37
- class Person < ActiveRecord::Base
38
- has_and_belongs_to_many :companies, :join_table => "people_companies"
39
- def self.table_name() "people" end
40
- end
41
-
42
-
43
- # Usage ---------------
44
-
45
- logger.info "\nCreate fixtures"
46
-
47
- Firm.new("name" => "Next Angle").save
48
- Client.new("name" => "37signals", "client_of" => 1).save
49
- Person.new("name" => "David").save
50
-
51
-
52
- logger.info "\nUsing Finders"
53
-
54
- next_angle = Company.find(1)
55
- next_angle = Firm.find(1)
56
- next_angle = Company.find_first "name = 'Next Angle'"
57
- next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first
58
-
59
- Firm === next_angle
60
-
61
-
62
- logger.info "\nUsing has_many association"
63
-
64
- next_angle.has_clients?
65
- next_angle.clients_count
66
- all_clients = next_angle.clients
67
-
68
- thirty_seven_signals = next_angle.find_in_clients(2)
69
-
70
-
71
- logger.info "\nUsing belongs_to association"
72
-
73
- thirty_seven_signals.has_firm?
74
- thirty_seven_signals.firm?(next_angle)
75
-
76
-
77
- logger.info "\nUsing has_and_belongs_to_many association"
78
-
79
- david = Person.find(1)
80
- david.add_companies(thirty_seven_signals, next_angle)
81
- david.companies.include?(next_angle)
82
- david.companies_count == 2
83
-
84
- david.remove_companies(next_angle)
85
- david.companies_count == 1
86
-
87
- thirty_seven_signals.people.include?(david)
@@ -1,15 +0,0 @@
1
- # Be sure to change the mysql_connection details and create a database for the example
2
-
3
- $: << File.dirname(__FILE__) + '/../lib'
4
-
5
- require 'active_record'
6
- require 'logger'; class Logger; def format_message(severity, timestamp, msg, progname) "#{msg}\n" end; end
7
-
8
- ActiveRecord::Base.logger = Logger.new(STDOUT)
9
- ActiveRecord::Base.establish_connection(
10
- :adapter => "mysql",
11
- :host => "localhost",
12
- :username => "root",
13
- :password => "",
14
- :database => "activerecord_examples"
15
- )
@@ -1,85 +0,0 @@
1
- require File.dirname(__FILE__) + '/shared_setup'
2
-
3
- logger = Logger.new(STDOUT)
4
-
5
- # Database setup ---------------
6
-
7
- logger.info "\nCreate tables"
8
-
9
- [ "DROP TABLE people",
10
- "CREATE TABLE people (id int(11) auto_increment, name varchar(100), pass varchar(100), email varchar(100), PRIMARY KEY (id))"
11
- ].each { |statement|
12
- begin; ActiveRecord::Base.connection.execute(statement); rescue ActiveRecord::StatementInvalid; end # Tables doesn't necessarily already exist
13
- }
14
-
15
-
16
- # Class setup ---------------
17
-
18
- class Person < ActiveRecord::Base
19
- # Using
20
- def self.authenticate(name, pass)
21
- # find_first "name = '#{name}' AND pass = '#{pass}'" would be open to sql-injection (in a web-app scenario)
22
- find_first [ "name = '%s' AND pass = '%s'", name, pass ]
23
- end
24
-
25
- def self.name_exists?(name, id = nil)
26
- if id.nil?
27
- condition = [ "name = '%s'", name ]
28
- else
29
- # Check if anyone else than the person identified by person_id has that user_name
30
- condition = [ "name = '%s' AND id <> %d", name, id ]
31
- end
32
-
33
- !find_first(condition).nil?
34
- end
35
-
36
- def email_address_with_name
37
- "\"#{name}\" <#{email}>"
38
- end
39
-
40
- protected
41
- def validate
42
- errors.add_on_empty(%w(name pass email))
43
- errors.add("email", "must be valid") unless email_address_valid?
44
- end
45
-
46
- def validate_on_create
47
- if attribute_present?("name") && Person.name_exists?(name)
48
- errors.add("name", "is already taken by another person")
49
- end
50
- end
51
-
52
- def validate_on_update
53
- if attribute_present?("name") && Person.name_exists?(name, id)
54
- errors.add("name", "is already taken by another person")
55
- end
56
- end
57
-
58
- private
59
- def email_address_valid?() email =~ /\w[-.\w]*\@[-\w]+[-.\w]*\.\w+/ end
60
- end
61
-
62
- # Usage ---------------
63
-
64
- logger.info "\nCreate fixtures"
65
- david = Person.new("name" => "David Heinemeier Hansson", "pass" => "", "email" => "")
66
- unless david.save
67
- puts "There was #{david.errors.count} error(s)"
68
- david.errors.each_full { |error| puts error }
69
- end
70
-
71
- david.pass = "something"
72
- david.email = "invalid_address"
73
- unless david.save
74
- puts "There was #{david.errors.count} error(s)"
75
- puts "It was email with: " + david.errors.on("email")
76
- end
77
-
78
- david.email = "david@loudthinking.com"
79
- if david.save then puts "David finally made it!" end
80
-
81
-
82
- another_david = Person.new("name" => "David Heinemeier Hansson", "pass" => "xc", "email" => "david@loudthinking")
83
- unless another_david.save
84
- puts "Error on name: " + another_david.errors.on("name")
85
- end
@@ -1,256 +0,0 @@
1
- module ActiveRecord
2
- module Acts #:nodoc:
3
- module List #:nodoc:
4
- def self.included(base)
5
- base.extend(ClassMethods)
6
- end
7
-
8
- # This act provides the capabilities for sorting and reordering a number of objects in a list.
9
- # The class that has this specified needs to have a "position" column defined as an integer on
10
- # the mapped database table.
11
- #
12
- # Todo list example:
13
- #
14
- # class TodoList < ActiveRecord::Base
15
- # has_many :todo_items, :order => "position"
16
- # end
17
- #
18
- # class TodoItem < ActiveRecord::Base
19
- # belongs_to :todo_list
20
- # acts_as_list :scope => :todo_list
21
- # end
22
- #
23
- # todo_list.first.move_to_bottom
24
- # todo_list.last.move_higher
25
- module ClassMethods
26
- # Configuration options are:
27
- #
28
- # * +column+ - specifies the column name to use for keeping the position integer (default: position)
29
- # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
30
- # (if that hasn't been already) and use that as the foreign key restriction. It's also possible
31
- # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32
- # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
- def acts_as_list(options = {})
34
- configuration = { :column => "position", :scope => "1 = 1" }
35
- configuration.update(options) if options.is_a?(Hash)
36
-
37
- configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
38
-
39
- if configuration[:scope].is_a?(Symbol)
40
- scope_condition_method = %(
41
- def scope_condition
42
- if #{configuration[:scope].to_s}.nil?
43
- "#{configuration[:scope].to_s} IS NULL"
44
- else
45
- "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
46
- end
47
- end
48
- )
49
- else
50
- scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
51
- end
52
-
53
- class_eval <<-EOV
54
- include ActiveRecord::Acts::List::InstanceMethods
55
-
56
- def acts_as_list_class
57
- ::#{self.name}
58
- end
59
-
60
- def position_column
61
- '#{configuration[:column]}'
62
- end
63
-
64
- #{scope_condition_method}
65
-
66
- before_destroy :remove_from_list
67
- before_create :add_to_list_bottom
68
- EOV
69
- end
70
- end
71
-
72
- # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
73
- # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
74
- # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return true if that chapter is
75
- # the first in the list of all chapters.
76
- module InstanceMethods
77
- # Insert the item at the given position (defaults to the top position of 1).
78
- def insert_at(position = 1)
79
- insert_at_position(position)
80
- end
81
-
82
- # Swap positions with the next lower item, if one exists.
83
- def move_lower
84
- return unless lower_item
85
-
86
- acts_as_list_class.transaction do
87
- lower_item.decrement_position
88
- increment_position
89
- end
90
- end
91
-
92
- # Swap positions with the next higher item, if one exists.
93
- def move_higher
94
- return unless higher_item
95
-
96
- acts_as_list_class.transaction do
97
- higher_item.increment_position
98
- decrement_position
99
- end
100
- end
101
-
102
- # Move to the bottom of the list. If the item is already in the list, the items below it have their
103
- # position adjusted accordingly.
104
- def move_to_bottom
105
- return unless in_list?
106
- acts_as_list_class.transaction do
107
- decrement_positions_on_lower_items
108
- assume_bottom_position
109
- end
110
- end
111
-
112
- # Move to the top of the list. If the item is already in the list, the items above it have their
113
- # position adjusted accordingly.
114
- def move_to_top
115
- return unless in_list?
116
- acts_as_list_class.transaction do
117
- increment_positions_on_higher_items
118
- assume_top_position
119
- end
120
- end
121
-
122
- # Removes the item from the list.
123
- def remove_from_list
124
- if in_list?
125
- decrement_positions_on_lower_items
126
- update_attribute position_column, nil
127
- end
128
- end
129
-
130
- # Increase the position of this item without adjusting the rest of the list.
131
- def increment_position
132
- return unless in_list?
133
- update_attribute position_column, self.send(position_column).to_i + 1
134
- end
135
-
136
- # Decrease the position of this item without adjusting the rest of the list.
137
- def decrement_position
138
- return unless in_list?
139
- update_attribute position_column, self.send(position_column).to_i - 1
140
- end
141
-
142
- # Return true if this object is the first in the list.
143
- def first?
144
- return false unless in_list?
145
- self.send(position_column) == 1
146
- end
147
-
148
- # Return true if this object is the last in the list.
149
- def last?
150
- return false unless in_list?
151
- self.send(position_column) == bottom_position_in_list
152
- end
153
-
154
- # Return the next higher item in the list.
155
- def higher_item
156
- return nil unless in_list?
157
- acts_as_list_class.find(:first, :conditions =>
158
- "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
159
- )
160
- end
161
-
162
- # Return the next lower item in the list.
163
- def lower_item
164
- return nil unless in_list?
165
- acts_as_list_class.find(:first, :conditions =>
166
- "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
167
- )
168
- end
169
-
170
- # Test if this record is in a list
171
- def in_list?
172
- !send(position_column).nil?
173
- end
174
-
175
- private
176
- def add_to_list_top
177
- increment_positions_on_all_items
178
- end
179
-
180
- def add_to_list_bottom
181
- self[position_column] = bottom_position_in_list.to_i + 1
182
- end
183
-
184
- # Overwrite this method to define the scope of the list changes
185
- def scope_condition() "1" end
186
-
187
- # Returns the bottom position number in the list.
188
- # bottom_position_in_list # => 2
189
- def bottom_position_in_list(except = nil)
190
- item = bottom_item(except)
191
- item ? item.send(position_column) : 0
192
- end
193
-
194
- # Returns the bottom item
195
- def bottom_item(except = nil)
196
- conditions = scope_condition
197
- conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
198
- acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
199
- end
200
-
201
- # Forces item to assume the bottom position in the list.
202
- def assume_bottom_position
203
- update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
204
- end
205
-
206
- # Forces item to assume the top position in the list.
207
- def assume_top_position
208
- update_attribute(position_column, 1)
209
- end
210
-
211
- # This has the effect of moving all the higher items up one.
212
- def decrement_positions_on_higher_items(position)
213
- acts_as_list_class.update_all(
214
- "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
215
- )
216
- end
217
-
218
- # This has the effect of moving all the lower items up one.
219
- def decrement_positions_on_lower_items
220
- return unless in_list?
221
- acts_as_list_class.update_all(
222
- "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
223
- )
224
- end
225
-
226
- # This has the effect of moving all the higher items down one.
227
- def increment_positions_on_higher_items
228
- return unless in_list?
229
- acts_as_list_class.update_all(
230
- "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
231
- )
232
- end
233
-
234
- # This has the effect of moving all the lower items down one.
235
- def increment_positions_on_lower_items(position)
236
- acts_as_list_class.update_all(
237
- "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
238
- )
239
- end
240
-
241
- # Increments position (<tt>position_column</tt>) of all items in the list.
242
- def increment_positions_on_all_items
243
- acts_as_list_class.update_all(
244
- "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
245
- )
246
- end
247
-
248
- def insert_at_position(position)
249
- remove_from_list
250
- increment_positions_on_lower_items(position)
251
- self.update_attribute(position_column, position)
252
- end
253
- end
254
- end
255
- end
256
- end
@@ -1,211 +0,0 @@
1
- module ActiveRecord
2
- module Acts #:nodoc:
3
- module NestedSet #:nodoc:
4
- def self.included(base)
5
- base.extend(ClassMethods)
6
- end
7
-
8
- # This acts provides Nested Set functionality. Nested Set is similiar to Tree, but with
9
- # the added feature that you can select the children and all of their descendents with
10
- # a single query. A good use case for this is a threaded post system, where you want
11
- # to display every reply to a comment without multiple selects.
12
- #
13
- # A google search for "Nested Set" should point you in the direction to explain the
14
- # database theory. I figured out a bunch of this from
15
- # http://threebit.net/tutorials/nestedset/tutorial1.html
16
- #
17
- # Instead of picturing a leaf node structure with children pointing back to their parent,
18
- # the best way to imagine how this works is to think of the parent entity surrounding all
19
- # of its children, and its parent surrounding it, etc. Assuming that they are lined up
20
- # horizontally, we store the left and right boundries in the database.
21
- #
22
- # Imagine:
23
- # root
24
- # |_ Child 1
25
- # |_ Child 1.1
26
- # |_ Child 1.2
27
- # |_ Child 2
28
- # |_ Child 2.1
29
- # |_ Child 2.2
30
- #
31
- # If my cirlces in circles description didn't make sense, check out this sweet
32
- # ASCII art:
33
- #
34
- # ___________________________________________________________________
35
- # | Root |
36
- # | ____________________________ ____________________________ |
37
- # | | Child 1 | | Child 2 | |
38
- # | | __________ _________ | | __________ _________ | |
39
- # | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | |
40
- # 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14
41
- # | |___________________________| |___________________________| |
42
- # |___________________________________________________________________|
43
- #
44
- # The numbers represent the left and right boundries. The table then might
45
- # look like this:
46
- # ID | PARENT | LEFT | RIGHT | DATA
47
- # 1 | 0 | 1 | 14 | root
48
- # 2 | 1 | 2 | 7 | Child 1
49
- # 3 | 2 | 3 | 4 | Child 1.1
50
- # 4 | 2 | 5 | 6 | Child 1.2
51
- # 5 | 1 | 8 | 13 | Child 2
52
- # 6 | 5 | 9 | 10 | Child 2.1
53
- # 7 | 5 | 11 | 12 | Child 2.2
54
- #
55
- # So, to get all children of an entry, you
56
- # SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT
57
- #
58
- # To get the count, it's (LEFT - RIGHT + 1)/2, etc.
59
- #
60
- # To get the direct parent, it falls back to using the PARENT_ID field.
61
- #
62
- # There are instance methods for all of these.
63
- #
64
- # The structure is good if you need to group things together; the downside is that
65
- # keeping data integrity is a pain, and both adding and removing an entry
66
- # require a full table write.
67
- #
68
- # This sets up a before_destroy trigger to prune the tree correctly if one of its
69
- # elements gets deleted.
70
- #
71
- module ClassMethods
72
- # Configuration options are:
73
- #
74
- # * +parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
75
- # * +left_column+ - column name for left boundry data, default "lft"
76
- # * +right_column+ - column name for right boundry data, default "rgt"
77
- # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
78
- # (if that hasn't been already) and use that as the foreign key restriction. It's also possible
79
- # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
80
- # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
81
- def acts_as_nested_set(options = {})
82
- configuration = { :parent_column => "parent_id", :left_column => "lft", :right_column => "rgt", :scope => "1 = 1" }
83
-
84
- configuration.update(options) if options.is_a?(Hash)
85
-
86
- configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
87
-
88
- if configuration[:scope].is_a?(Symbol)
89
- scope_condition_method = %(
90
- def scope_condition
91
- if #{configuration[:scope].to_s}.nil?
92
- "#{configuration[:scope].to_s} IS NULL"
93
- else
94
- "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
95
- end
96
- end
97
- )
98
- else
99
- scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
100
- end
101
-
102
- class_eval <<-EOV
103
- include ActiveRecord::Acts::NestedSet::InstanceMethods
104
-
105
- #{scope_condition_method}
106
-
107
- def left_col_name() "#{configuration[:left_column]}" end
108
-
109
- def right_col_name() "#{configuration[:right_column]}" end
110
-
111
- def parent_column() "#{configuration[:parent_column]}" end
112
-
113
- EOV
114
- end
115
- end
116
-
117
- module InstanceMethods
118
- # Returns true is this is a root node.
119
- def root?
120
- parent_id = self[parent_column]
121
- (parent_id == 0 || parent_id.nil?) && (self[left_col_name] == 1) && (self[right_col_name] > self[left_col_name])
122
- end
123
-
124
- # Returns true is this is a child node
125
- def child?
126
- parent_id = self[parent_column]
127
- !(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name])
128
- end
129
-
130
- # Returns true if we have no idea what this is
131
- def unknown?
132
- !root? && !child?
133
- end
134
-
135
-
136
- # Adds a child to this object in the tree. If this object hasn't been initialized,
137
- # it gets set up as a root node. Otherwise, this method will update all of the
138
- # other elements in the tree and shift them to the right, keeping everything
139
- # balanced.
140
- def add_child( child )
141
- self.reload
142
- child.reload
143
-
144
- if child.root?
145
- raise "Adding sub-tree isn\'t currently supported"
146
- else
147
- if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) )
148
- # Looks like we're now the root node! Woo
149
- self[left_col_name] = 1
150
- self[right_col_name] = 4
151
-
152
- # What do to do about validation?
153
- return nil unless self.save
154
-
155
- child[parent_column] = self.id
156
- child[left_col_name] = 2
157
- child[right_col_name]= 3
158
- return child.save
159
- else
160
- # OK, we need to add and shift everything else to the right
161
- child[parent_column] = self.id
162
- right_bound = self[right_col_name]
163
- child[left_col_name] = right_bound
164
- child[right_col_name] = right_bound + 1
165
- self[right_col_name] += 2
166
- self.class.base_class.transaction {
167
- self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" )
168
- self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" )
169
- self.save
170
- child.save
171
- }
172
- end
173
- end
174
- end
175
-
176
- # Returns the number of nested children of this object.
177
- def children_count
178
- return (self[right_col_name] - self[left_col_name] - 1)/2
179
- end
180
-
181
- # Returns a set of itself and all of its nested children
182
- def full_set
183
- self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" )
184
- end
185
-
186
- # Returns a set of all of its children and nested children
187
- def all_children
188
- self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" )
189
- end
190
-
191
- # Returns a set of only this entry's immediate children
192
- def direct_children
193
- self.class.base_class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}")
194
- end
195
-
196
- # Prunes a branch off of the tree, shifting all of the elements on the right
197
- # back to the left so the counts still work.
198
- def before_destroy
199
- return if self[right_col_name].nil? || self[left_col_name].nil?
200
- dif = self[right_col_name] - self[left_col_name] + 1
201
-
202
- self.class.base_class.transaction {
203
- self.class.base_class.delete_all( "#{scope_condition} and #{left_col_name} > #{self[left_col_name]} and #{right_col_name} < #{self[right_col_name]}" )
204
- self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} - #{dif})", "#{scope_condition} AND #{left_col_name} >= #{self[right_col_name]}" )
205
- self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} - #{dif} )", "#{scope_condition} AND #{right_col_name} >= #{self[right_col_name]}" )
206
- }
207
- end
208
- end
209
- end
210
- end
211
- end