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.
- data/CHANGELOG +2454 -34
- data/README +1 -1
- data/RUNNING_UNIT_TESTS +3 -34
- data/Rakefile +98 -77
- data/install.rb +1 -1
- data/lib/active_record.rb +13 -22
- data/lib/active_record/aggregations.rb +38 -49
- data/lib/active_record/associations.rb +452 -333
- data/lib/active_record/associations/association_collection.rb +66 -20
- data/lib/active_record/associations/association_proxy.rb +9 -8
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +46 -51
- data/lib/active_record/associations/has_many_association.rb +21 -57
- data/lib/active_record/associations/has_many_through_association.rb +38 -18
- data/lib/active_record/associations/has_one_association.rb +30 -14
- data/lib/active_record/attribute_methods.rb +253 -0
- data/lib/active_record/base.rb +719 -494
- data/lib/active_record/calculations.rb +62 -63
- data/lib/active_record/callbacks.rb +57 -83
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +38 -9
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +56 -15
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +87 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +23 -12
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +191 -62
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +37 -34
- data/lib/active_record/connection_adapters/abstract_adapter.rb +28 -17
- data/lib/active_record/connection_adapters/mysql_adapter.rb +119 -37
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +473 -210
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +91 -107
- data/lib/active_record/fixtures.rb +503 -113
- data/lib/active_record/locking/optimistic.rb +72 -34
- data/lib/active_record/migration.rb +80 -57
- data/lib/active_record/observer.rb +13 -10
- data/lib/active_record/query_cache.rb +16 -57
- data/lib/active_record/reflection.rb +35 -38
- data/lib/active_record/schema.rb +5 -5
- data/lib/active_record/schema_dumper.rb +35 -13
- data/lib/active_record/serialization.rb +98 -0
- data/lib/active_record/serializers/json_serializer.rb +71 -0
- data/lib/active_record/{xml_serialization.rb → serializers/xml_serializer.rb} +90 -83
- data/lib/active_record/timestamp.rb +20 -21
- data/lib/active_record/transactions.rb +39 -43
- data/lib/active_record/validations.rb +256 -107
- data/lib/active_record/version.rb +3 -3
- data/lib/activerecord.rb +1 -0
- data/test/aaa_create_tables_test.rb +15 -2
- data/test/abstract_unit.rb +24 -17
- data/test/active_schema_test_mysql.rb +20 -8
- data/test/adapter_test.rb +23 -5
- data/test/adapter_test_sqlserver.rb +15 -1
- data/test/aggregations_test.rb +16 -1
- data/test/all.sh +2 -2
- data/test/associations/ar_joins_test.rb +0 -0
- data/test/associations/callbacks_test.rb +51 -30
- data/test/associations/cascaded_eager_loading_test.rb +1 -29
- data/test/associations/eager_singularization_test.rb +145 -0
- data/test/associations/eager_test.rb +42 -6
- data/test/associations/extension_test.rb +6 -1
- data/test/associations/inner_join_association_test.rb +88 -0
- data/test/associations/join_model_test.rb +47 -16
- data/test/associations_test.rb +449 -226
- data/test/attribute_methods_test.rb +97 -0
- data/test/base_test.rb +251 -105
- data/test/binary_test.rb +22 -27
- data/test/calculations_test.rb +37 -5
- data/test/callbacks_test.rb +23 -0
- data/test/connection_test_firebird.rb +2 -2
- data/test/connection_test_mysql.rb +30 -0
- data/test/connections/native_mysql/connection.rb +3 -0
- data/test/connections/native_sqlite/connection.rb +5 -14
- data/test/connections/native_sqlite3/connection.rb +5 -14
- data/test/connections/native_sqlite3/in_memory_connection.rb +1 -1
- data/test/{copy_table_sqlite.rb → copy_table_test_sqlite.rb} +8 -3
- data/test/datatype_test_postgresql.rb +178 -27
- data/test/{empty_date_time_test.rb → date_time_test.rb} +13 -1
- data/test/defaults_test.rb +8 -1
- data/test/deprecated_finder_test.rb +7 -128
- data/test/finder_test.rb +192 -54
- data/test/fixtures/all/developers.yml +0 -0
- data/test/fixtures/all/people.csv +0 -0
- data/test/fixtures/all/tasks.yml +0 -0
- data/test/fixtures/author.rb +12 -5
- data/test/fixtures/binaries.yml +130 -435
- data/test/fixtures/category.rb +6 -0
- data/test/fixtures/company.rb +8 -1
- data/test/fixtures/computer.rb +1 -0
- data/test/fixtures/contact.rb +16 -0
- data/test/fixtures/customer.rb +2 -2
- data/test/fixtures/db_definitions/db2.drop.sql +1 -0
- data/test/fixtures/db_definitions/db2.sql +4 -0
- data/test/fixtures/db_definitions/firebird.drop.sql +3 -1
- data/test/fixtures/db_definitions/firebird.sql +6 -0
- data/test/fixtures/db_definitions/frontbase.drop.sql +1 -0
- data/test/fixtures/db_definitions/frontbase.sql +5 -0
- data/test/fixtures/db_definitions/openbase.sql +41 -25
- data/test/fixtures/db_definitions/oracle.drop.sql +2 -0
- data/test/fixtures/db_definitions/oracle.sql +5 -0
- data/test/fixtures/db_definitions/postgresql.drop.sql +7 -0
- data/test/fixtures/db_definitions/postgresql.sql +87 -58
- data/test/fixtures/db_definitions/postgresql2.sql +1 -2
- data/test/fixtures/db_definitions/schema.rb +280 -0
- data/test/fixtures/db_definitions/schema2.rb +11 -0
- data/test/fixtures/db_definitions/sqlite.drop.sql +1 -0
- data/test/fixtures/db_definitions/sqlite.sql +4 -0
- data/test/fixtures/db_definitions/sybase.drop.sql +1 -0
- data/test/fixtures/db_definitions/sybase.sql +4 -0
- data/test/fixtures/developer.rb +10 -0
- data/test/fixtures/example.log +1 -0
- data/test/fixtures/flowers.jpg +0 -0
- data/test/fixtures/item.rb +7 -0
- data/test/fixtures/items.yml +4 -0
- data/test/fixtures/joke.rb +0 -3
- data/test/fixtures/matey.rb +4 -0
- data/test/fixtures/mateys.yml +4 -0
- data/test/fixtures/minimalistic.rb +2 -0
- data/test/fixtures/minimalistics.yml +2 -0
- data/test/fixtures/mixins.yml +2 -100
- data/test/fixtures/parrot.rb +13 -0
- data/test/fixtures/parrots.yml +27 -0
- data/test/fixtures/parrots_pirates.yml +7 -0
- data/test/fixtures/pirate.rb +5 -0
- data/test/fixtures/pirates.yml +9 -0
- data/test/fixtures/post.rb +1 -0
- data/test/fixtures/project.rb +3 -2
- data/test/fixtures/reserved_words/distinct.yml +5 -0
- data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
- data/test/fixtures/reserved_words/group.yml +14 -0
- data/test/fixtures/reserved_words/select.yml +8 -0
- data/test/fixtures/reserved_words/values.yml +7 -0
- data/test/fixtures/ship.rb +3 -0
- data/test/fixtures/ships.yml +5 -0
- data/test/fixtures/tagging.rb +4 -0
- data/test/fixtures/taggings.yml +8 -1
- data/test/fixtures/topic.rb +13 -1
- data/test/fixtures/treasure.rb +4 -0
- data/test/fixtures/treasures.yml +10 -0
- data/test/fixtures_test.rb +205 -24
- data/test/inheritance_test.rb +7 -1
- data/test/json_serialization_test.rb +180 -0
- data/test/lifecycle_test.rb +1 -1
- data/test/locking_test.rb +85 -2
- data/test/migration_test.rb +206 -40
- data/test/mixin_test.rb +13 -515
- data/test/pk_test.rb +3 -6
- data/test/query_cache_test.rb +104 -0
- data/test/reflection_test.rb +16 -0
- data/test/reserved_word_test_mysql.rb +177 -0
- data/test/schema_dumper_test.rb +38 -3
- data/test/serialization_test.rb +47 -0
- data/test/transactions_test.rb +74 -23
- data/test/unconnected_test.rb +1 -1
- data/test/validations_test.rb +322 -32
- data/test/xml_serialization_test.rb +121 -44
- metadata +48 -41
- data/examples/associations.rb +0 -87
- data/examples/shared_setup.rb +0 -15
- data/examples/validation.rb +0 -85
- data/lib/active_record/acts/list.rb +0 -256
- data/lib/active_record/acts/nested_set.rb +0 -211
- data/lib/active_record/acts/tree.rb +0 -96
- data/lib/active_record/connection_adapters/db2_adapter.rb +0 -228
- data/lib/active_record/connection_adapters/firebird_adapter.rb +0 -728
- data/lib/active_record/connection_adapters/frontbase_adapter.rb +0 -861
- data/lib/active_record/connection_adapters/openbase_adapter.rb +0 -350
- data/lib/active_record/connection_adapters/oracle_adapter.rb +0 -690
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +0 -591
- data/lib/active_record/connection_adapters/sybase_adapter.rb +0 -662
- data/lib/active_record/deprecated_associations.rb +0 -104
- data/lib/active_record/deprecated_finders.rb +0 -44
- data/lib/active_record/vendor/simple.rb +0 -693
- data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
- data/lib/active_record/wrappings.rb +0 -58
- data/test/connections/native_sqlserver/connection.rb +0 -23
- data/test/connections/native_sqlserver_odbc/connection.rb +0 -25
- data/test/deprecated_associations_test.rb +0 -396
- data/test/fixtures/db_definitions/mysql.drop.sql +0 -32
- data/test/fixtures/db_definitions/mysql.sql +0 -234
- data/test/fixtures/db_definitions/mysql2.drop.sql +0 -2
- data/test/fixtures/db_definitions/mysql2.sql +0 -5
- data/test/fixtures/db_definitions/sqlserver.drop.sql +0 -34
- data/test/fixtures/db_definitions/sqlserver.sql +0 -243
- data/test/fixtures/db_definitions/sqlserver2.drop.sql +0 -2
- data/test/fixtures/db_definitions/sqlserver2.sql +0 -5
- data/test/fixtures/mixin.rb +0 -63
- data/test/mixin_nested_set_test.rb +0 -196
data/examples/associations.rb
DELETED
@@ -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)
|
data/examples/shared_setup.rb
DELETED
@@ -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
|
-
)
|
data/examples/validation.rb
DELETED
@@ -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
|