activerecord 2.1.2 → 2.2.2
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 +32 -6
- data/README +0 -0
- data/Rakefile +4 -5
- data/lib/active_record.rb +11 -10
- data/lib/active_record/aggregations.rb +110 -38
- data/lib/active_record/association_preload.rb +104 -15
- data/lib/active_record/associations.rb +427 -212
- data/lib/active_record/associations/association_collection.rb +101 -16
- data/lib/active_record/associations/association_proxy.rb +65 -13
- data/lib/active_record/associations/belongs_to_association.rb +2 -2
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +0 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +13 -3
- data/lib/active_record/associations/has_many_association.rb +28 -28
- data/lib/active_record/associations/has_many_through_association.rb +21 -19
- data/lib/active_record/associations/has_one_association.rb +24 -7
- data/lib/active_record/associations/has_one_through_association.rb +3 -4
- data/lib/active_record/attribute_methods.rb +13 -5
- data/lib/active_record/base.rb +435 -212
- data/lib/active_record/calculations.rb +12 -5
- data/lib/active_record/callbacks.rb +28 -9
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +355 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +42 -215
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -5
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +2 -1
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +48 -7
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +10 -4
- data/lib/active_record/connection_adapters/abstract_adapter.rb +67 -26
- data/lib/active_record/connection_adapters/mysql_adapter.rb +71 -45
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +155 -84
- data/lib/active_record/dirty.rb +25 -7
- data/lib/active_record/dynamic_finder_match.rb +41 -0
- data/lib/active_record/fixtures.rb +10 -9
- data/lib/active_record/i18n_interpolation_deprecation.rb +26 -0
- data/lib/active_record/locale/en.yml +54 -0
- data/lib/active_record/migration.rb +47 -10
- data/lib/active_record/named_scope.rb +29 -16
- data/lib/active_record/reflection.rb +118 -54
- data/lib/active_record/schema_dumper.rb +13 -7
- data/lib/active_record/test_case.rb +18 -5
- data/lib/active_record/transactions.rb +89 -34
- data/lib/active_record/validations.rb +270 -180
- data/lib/active_record/version.rb +1 -1
- data/test/cases/active_schema_test_mysql.rb +5 -0
- data/test/cases/adapter_test.rb +6 -0
- data/test/cases/aggregations_test.rb +39 -0
- data/test/cases/associations/belongs_to_associations_test.rb +10 -0
- data/test/cases/associations/eager_load_nested_include_test.rb +30 -12
- data/test/cases/associations/eager_test.rb +54 -5
- data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +77 -10
- data/test/cases/associations/has_many_associations_test.rb +74 -7
- data/test/cases/associations/has_many_through_associations_test.rb +50 -3
- data/test/cases/associations/has_one_associations_test.rb +17 -0
- data/test/cases/associations/has_one_through_associations_test.rb +49 -1
- data/test/cases/associations_test.rb +0 -0
- data/test/cases/attribute_methods_test.rb +59 -4
- data/test/cases/base_test.rb +93 -21
- data/test/cases/binary_test.rb +1 -5
- data/test/cases/calculations_test.rb +5 -0
- data/test/cases/callbacks_observers_test.rb +38 -0
- data/test/cases/connection_test_mysql.rb +1 -1
- data/test/cases/defaults_test.rb +32 -1
- data/test/cases/deprecated_finder_test.rb +0 -0
- data/test/cases/dirty_test.rb +13 -0
- data/test/cases/finder_test.rb +162 -12
- data/test/cases/fixtures_test.rb +32 -3
- data/test/cases/helper.rb +15 -0
- data/test/cases/i18n_test.rb +41 -0
- data/test/cases/inheritance_test.rb +2 -2
- data/test/cases/lifecycle_test.rb +0 -0
- data/test/cases/locking_test.rb +4 -9
- data/test/cases/method_scoping_test.rb +109 -2
- data/test/cases/migration_test.rb +43 -8
- data/test/cases/multiple_db_test.rb +25 -0
- data/test/cases/named_scope_test.rb +74 -0
- data/test/cases/pooled_connections_test.rb +103 -0
- data/test/cases/readonly_test.rb +0 -0
- data/test/cases/reflection_test.rb +11 -3
- data/test/cases/reload_models_test.rb +20 -0
- data/test/cases/sanitize_test.rb +25 -0
- data/test/cases/schema_authorization_test_postgresql.rb +2 -2
- data/test/cases/transactions_test.rb +62 -12
- data/test/cases/unconnected_test.rb +0 -0
- data/test/cases/validations_i18n_test.rb +921 -0
- data/test/cases/validations_test.rb +44 -33
- data/test/connections/native_mysql/connection.rb +1 -3
- data/test/fixtures/companies.yml +1 -0
- data/test/fixtures/customers.yml +10 -1
- data/test/fixtures/fixture_database.sqlite3 +0 -0
- data/test/fixtures/fixture_database_2.sqlite3 +0 -0
- data/test/fixtures/organizations.yml +5 -0
- data/test/migrations/broken/100_migration_that_raises_exception.rb +10 -0
- data/test/models/author.rb +3 -0
- data/test/models/category.rb +3 -0
- data/test/models/club.rb +6 -0
- data/test/models/company.rb +25 -1
- data/test/models/customer.rb +19 -1
- data/test/models/member.rb +2 -0
- data/test/models/member_detail.rb +4 -0
- data/test/models/organization.rb +4 -0
- data/test/models/parrot.rb +1 -0
- data/test/models/post.rb +3 -0
- data/test/models/reply.rb +0 -0
- data/test/models/topic.rb +3 -0
- data/test/schema/schema.rb +12 -1
- metadata +22 -10
- data/lib/active_record/vendor/mysql.rb +0 -1214
- data/test/cases/adapter_test_sqlserver.rb +0 -95
- data/test/cases/table_name_test_sqlserver.rb +0 -23
- data/test/cases/threaded_connections_test.rb +0 -48
- data/test/schema/sqlserver_specific_schema.rb +0 -5
data/lib/active_record/dirty.rb
CHANGED
@@ -34,8 +34,10 @@ module ActiveRecord
|
|
34
34
|
# person.name << 'by'
|
35
35
|
# person.name_change # => ['uncle bob', 'uncle bobby']
|
36
36
|
module Dirty
|
37
|
+
DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
|
38
|
+
|
37
39
|
def self.included(base)
|
38
|
-
base.attribute_method_suffix
|
40
|
+
base.attribute_method_suffix *DIRTY_SUFFIXES
|
39
41
|
base.alias_method_chain :write_attribute, :dirty
|
40
42
|
base.alias_method_chain :save, :dirty
|
41
43
|
base.alias_method_chain :save!, :dirty
|
@@ -44,6 +46,8 @@ module ActiveRecord
|
|
44
46
|
|
45
47
|
base.superclass_delegating_accessor :partial_updates
|
46
48
|
base.partial_updates = true
|
49
|
+
|
50
|
+
base.send(:extend, ClassMethods)
|
47
51
|
end
|
48
52
|
|
49
53
|
# Do any attributes have unsaved changes?
|
@@ -62,7 +66,7 @@ module ActiveRecord
|
|
62
66
|
changed_attributes.keys
|
63
67
|
end
|
64
68
|
|
65
|
-
# Map of changed attrs => [original value, new value]
|
69
|
+
# Map of changed attrs => [original value, new value].
|
66
70
|
# person.changes # => {}
|
67
71
|
# person.name = 'bob'
|
68
72
|
# person.changes # => { 'name' => ['bill', 'bob'] }
|
@@ -93,27 +97,27 @@ module ActiveRecord
|
|
93
97
|
end
|
94
98
|
|
95
99
|
private
|
96
|
-
# Map of change attr => original value
|
100
|
+
# Map of change <tt>attr => original value</tt>.
|
97
101
|
def changed_attributes
|
98
102
|
@changed_attributes ||= {}
|
99
103
|
end
|
100
104
|
|
101
|
-
# Handle
|
105
|
+
# Handle <tt>*_changed?</tt> for +method_missing+.
|
102
106
|
def attribute_changed?(attr)
|
103
107
|
changed_attributes.include?(attr)
|
104
108
|
end
|
105
109
|
|
106
|
-
# Handle
|
110
|
+
# Handle <tt>*_change</tt> for +method_missing+.
|
107
111
|
def attribute_change(attr)
|
108
112
|
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
109
113
|
end
|
110
114
|
|
111
|
-
# Handle
|
115
|
+
# Handle <tt>*_was</tt> for +method_missing+.
|
112
116
|
def attribute_was(attr)
|
113
117
|
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
114
118
|
end
|
115
119
|
|
116
|
-
# Handle
|
120
|
+
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
117
121
|
def attribute_will_change!(attr)
|
118
122
|
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
|
119
123
|
end
|
@@ -161,5 +165,19 @@ module ActiveRecord
|
|
161
165
|
old != value
|
162
166
|
end
|
163
167
|
|
168
|
+
module ClassMethods
|
169
|
+
def self.extended(base)
|
170
|
+
base.metaclass.alias_method_chain(:alias_attribute, :dirty)
|
171
|
+
end
|
172
|
+
|
173
|
+
def alias_attribute_with_dirty(new_name, old_name)
|
174
|
+
alias_attribute_without_dirty(new_name, old_name)
|
175
|
+
DIRTY_SUFFIXES.each do |suffix|
|
176
|
+
module_eval <<-STR, __FILE__, __LINE__+1
|
177
|
+
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end
|
178
|
+
STR
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
164
182
|
end
|
165
183
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class DynamicFinderMatch
|
3
|
+
def self.match(method)
|
4
|
+
df_match = self.new(method)
|
5
|
+
df_match.finder ? df_match : nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(method)
|
9
|
+
@finder = :first
|
10
|
+
case method.to_s
|
11
|
+
when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
|
12
|
+
@finder = :last if $1 == 'last_by'
|
13
|
+
@finder = :all if $1 == 'all_by'
|
14
|
+
names = $2
|
15
|
+
when /^find_by_([_a-zA-Z]\w*)\!$/
|
16
|
+
@bang = true
|
17
|
+
names = $1
|
18
|
+
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
19
|
+
@instantiator = $1 == 'initialize' ? :new : :create
|
20
|
+
names = $2
|
21
|
+
else
|
22
|
+
@finder = nil
|
23
|
+
end
|
24
|
+
@attribute_names = names && names.split('_and_')
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :finder, :attribute_names, :instantiator
|
28
|
+
|
29
|
+
def finder?
|
30
|
+
!@finder.nil? && @instantiator.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def instantiator?
|
34
|
+
@finder == :first && !@instantiator.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def bang?
|
38
|
+
@bang
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -515,7 +515,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
|
|
515
515
|
|
516
516
|
all_loaded_fixtures.update(fixtures_map)
|
517
517
|
|
518
|
-
connection.transaction(
|
518
|
+
connection.transaction(connection.open_transactions.zero?) do
|
519
519
|
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
|
520
520
|
fixtures.each { |fixture| fixture.insert_fixtures }
|
521
521
|
|
@@ -541,13 +541,14 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
|
|
541
541
|
label.to_s.hash.abs
|
542
542
|
end
|
543
543
|
|
544
|
-
attr_reader :table_name
|
544
|
+
attr_reader :table_name, :name
|
545
545
|
|
546
546
|
def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
|
547
547
|
@connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
|
548
|
+
@name = table_name # preserve fixture base name
|
548
549
|
@class_name = class_name ||
|
549
550
|
(ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
|
550
|
-
@table_name = ActiveRecord::Base.table_name_prefix
|
551
|
+
@table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}"
|
551
552
|
@table_name = class_name.table_name if class_name.respond_to?(:table_name)
|
552
553
|
@connection = class_name.connection if class_name.respond_to?(:connection)
|
553
554
|
read_fixture_files
|
@@ -929,7 +930,7 @@ module Test #:nodoc:
|
|
929
930
|
load_fixtures
|
930
931
|
@@already_loaded_fixtures[self.class] = @loaded_fixtures
|
931
932
|
end
|
932
|
-
ActiveRecord::Base.
|
933
|
+
ActiveRecord::Base.connection.increment_open_transactions
|
933
934
|
ActiveRecord::Base.connection.begin_db_transaction
|
934
935
|
# Load fixtures for every test.
|
935
936
|
else
|
@@ -950,11 +951,11 @@ module Test #:nodoc:
|
|
950
951
|
end
|
951
952
|
|
952
953
|
# Rollback changes if a transaction is active.
|
953
|
-
if use_transactional_fixtures? &&
|
954
|
+
if use_transactional_fixtures? && ActiveRecord::Base.connection.open_transactions != 0
|
954
955
|
ActiveRecord::Base.connection.rollback_db_transaction
|
955
|
-
|
956
|
+
ActiveRecord::Base.connection.decrement_open_transactions
|
956
957
|
end
|
957
|
-
ActiveRecord::Base.
|
958
|
+
ActiveRecord::Base.clear_active_connections!
|
958
959
|
end
|
959
960
|
|
960
961
|
private
|
@@ -963,9 +964,9 @@ module Test #:nodoc:
|
|
963
964
|
fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
|
964
965
|
unless fixtures.nil?
|
965
966
|
if fixtures.instance_of?(Fixtures)
|
966
|
-
@loaded_fixtures[fixtures.
|
967
|
+
@loaded_fixtures[fixtures.name] = fixtures
|
967
968
|
else
|
968
|
-
fixtures.each { |f| @loaded_fixtures[f.
|
969
|
+
fixtures.each { |f| @loaded_fixtures[f.name] = f }
|
969
970
|
end
|
970
971
|
end
|
971
972
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Deprecates the use of the former message interpolation syntax in activerecord
|
2
|
+
# as in "must have %d characters". The new syntax uses explicit variable names
|
3
|
+
# as in "{{value}} must have {{count}} characters".
|
4
|
+
|
5
|
+
require 'i18n/backend/simple'
|
6
|
+
module I18n
|
7
|
+
module Backend
|
8
|
+
class Simple
|
9
|
+
DEPRECATED_INTERPOLATORS = { '%d' => '{{count}}', '%s' => '{{value}}' }
|
10
|
+
|
11
|
+
protected
|
12
|
+
def interpolate_with_deprecated_syntax(locale, string, values = {})
|
13
|
+
return string unless string.is_a?(String)
|
14
|
+
|
15
|
+
string = string.gsub(/%d|%s/) do |s|
|
16
|
+
instead = DEPRECATED_INTERPOLATORS[s]
|
17
|
+
ActiveSupport::Deprecation.warn "using #{s} in messages is deprecated; use #{instead} instead."
|
18
|
+
instead
|
19
|
+
end
|
20
|
+
|
21
|
+
interpolate_without_deprecated_syntax(locale, string, values)
|
22
|
+
end
|
23
|
+
alias_method_chain :interpolate, :deprecated_syntax
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
en:
|
2
|
+
activerecord:
|
3
|
+
errors:
|
4
|
+
# The values :model, :attribute and :value are always available for interpolation
|
5
|
+
# The value :count is available when applicable. Can be used for pluralization.
|
6
|
+
messages:
|
7
|
+
inclusion: "is not included in the list"
|
8
|
+
exclusion: "is reserved"
|
9
|
+
invalid: "is invalid"
|
10
|
+
confirmation: "doesn't match confirmation"
|
11
|
+
accepted: "must be accepted"
|
12
|
+
empty: "can't be empty"
|
13
|
+
blank: "can't be blank"
|
14
|
+
too_long: "is too long (maximum is {{count}} characters)"
|
15
|
+
too_short: "is too short (minimum is {{count}} characters)"
|
16
|
+
wrong_length: "is the wrong length (should be {{count}} characters)"
|
17
|
+
taken: "has already been taken"
|
18
|
+
not_a_number: "is not a number"
|
19
|
+
greater_than: "must be greater than {{count}}"
|
20
|
+
greater_than_or_equal_to: "must be greater than or equal to {{count}}"
|
21
|
+
equal_to: "must be equal to {{count}}"
|
22
|
+
less_than: "must be less than {{count}}"
|
23
|
+
less_than_or_equal_to: "must be less than or equal to {{count}}"
|
24
|
+
odd: "must be odd"
|
25
|
+
even: "must be even"
|
26
|
+
# Append your own errors here or at the model/attributes scope.
|
27
|
+
|
28
|
+
# You can define own errors for models or model attributes.
|
29
|
+
# The values :model, :attribute and :value are always available for interpolation.
|
30
|
+
#
|
31
|
+
# For example,
|
32
|
+
# models:
|
33
|
+
# user:
|
34
|
+
# blank: "This is a custom blank message for {{model}}: {{attribute}}"
|
35
|
+
# attributes:
|
36
|
+
# login:
|
37
|
+
# blank: "This is a custom blank message for User login"
|
38
|
+
# Will define custom blank validation message for User model and
|
39
|
+
# custom blank validation message for login attribute of User model.
|
40
|
+
models:
|
41
|
+
|
42
|
+
# Translate model names. Used in Model.human_name().
|
43
|
+
#models:
|
44
|
+
# For example,
|
45
|
+
# user: "Dude"
|
46
|
+
# will translate User model name to "Dude"
|
47
|
+
|
48
|
+
# Translate model attribute names. Used in Model.human_attribute_name(attribute).
|
49
|
+
#attributes:
|
50
|
+
# For example,
|
51
|
+
# user:
|
52
|
+
# login: "Handle"
|
53
|
+
# will translate User attribute "login" as "Handle"
|
54
|
+
|
@@ -349,6 +349,27 @@ module ActiveRecord
|
|
349
349
|
end
|
350
350
|
end
|
351
351
|
|
352
|
+
# MigrationProxy is used to defer loading of the actual migration classes
|
353
|
+
# until they are needed
|
354
|
+
class MigrationProxy
|
355
|
+
|
356
|
+
attr_accessor :name, :version, :filename
|
357
|
+
|
358
|
+
delegate :migrate, :announce, :write, :to=>:migration
|
359
|
+
|
360
|
+
private
|
361
|
+
|
362
|
+
def migration
|
363
|
+
@migration ||= load_migration
|
364
|
+
end
|
365
|
+
|
366
|
+
def load_migration
|
367
|
+
load(filename)
|
368
|
+
name.constantize
|
369
|
+
end
|
370
|
+
|
371
|
+
end
|
372
|
+
|
352
373
|
class Migrator#:nodoc:
|
353
374
|
class << self
|
354
375
|
def migrate(migrations_path, target_version = nil)
|
@@ -443,17 +464,25 @@ module ActiveRecord
|
|
443
464
|
runnable.pop if down? && !target.nil?
|
444
465
|
|
445
466
|
runnable.each do |migration|
|
446
|
-
Base.logger.info "Migrating to #{migration} (#{migration.version})"
|
467
|
+
Base.logger.info "Migrating to #{migration.name} (#{migration.version})"
|
447
468
|
|
448
469
|
# On our way up, we skip migrating the ones we've already migrated
|
449
|
-
# On our way down, we skip reverting the ones we've never migrated
|
450
470
|
next if up? && migrated.include?(migration.version.to_i)
|
451
471
|
|
472
|
+
# On our way down, we skip reverting the ones we've never migrated
|
452
473
|
if down? && !migrated.include?(migration.version.to_i)
|
453
474
|
migration.announce 'never migrated, skipping'; migration.write
|
454
|
-
|
455
|
-
|
456
|
-
|
475
|
+
next
|
476
|
+
end
|
477
|
+
|
478
|
+
begin
|
479
|
+
ddl_transaction do
|
480
|
+
migration.migrate(@direction)
|
481
|
+
record_version_state_after_migrating(migration.version)
|
482
|
+
end
|
483
|
+
rescue => e
|
484
|
+
canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
|
485
|
+
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
|
457
486
|
end
|
458
487
|
end
|
459
488
|
end
|
@@ -476,11 +505,10 @@ module ActiveRecord
|
|
476
505
|
raise DuplicateMigrationNameError.new(name.camelize)
|
477
506
|
end
|
478
507
|
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
klass.version = version
|
508
|
+
klasses << returning(MigrationProxy.new) do |migration|
|
509
|
+
migration.name = name.camelize
|
510
|
+
migration.version = version
|
511
|
+
migration.filename = file
|
484
512
|
end
|
485
513
|
end
|
486
514
|
|
@@ -519,5 +547,14 @@ module ActiveRecord
|
|
519
547
|
def down?
|
520
548
|
@direction == :down
|
521
549
|
end
|
550
|
+
|
551
|
+
# Wrap the migration in a transaction only if supported by the adapter.
|
552
|
+
def ddl_transaction(&block)
|
553
|
+
if Base.connection.supports_ddl_transactions?
|
554
|
+
Base.transaction { block.call }
|
555
|
+
else
|
556
|
+
block.call
|
557
|
+
end
|
558
|
+
end
|
522
559
|
end
|
523
560
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module NamedScope
|
3
|
-
# All subclasses of ActiveRecord::Base have two
|
4
|
-
# * <tt>all</tt
|
5
|
-
# * <tt>scoped</tt
|
3
|
+
# All subclasses of ActiveRecord::Base have two named \scopes:
|
4
|
+
# * <tt>all</tt> - which is similar to a <tt>find(:all)</tt> query, and
|
5
|
+
# * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
|
6
6
|
#
|
7
|
-
# These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
|
7
|
+
# These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
|
8
8
|
# intermediate values (scopes) around as first-class objects is convenient.
|
9
9
|
def self.included(base)
|
10
10
|
base.class_eval do
|
@@ -26,20 +26,20 @@ module ActiveRecord
|
|
26
26
|
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
|
27
27
|
# end
|
28
28
|
#
|
29
|
-
# The above calls to <tt>named_scope</tt> define class methods
|
29
|
+
# The above calls to <tt>named_scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
|
30
30
|
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
|
31
31
|
#
|
32
|
-
# Unlike Shirt.find(...)
|
32
|
+
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it resembles the association object
|
33
33
|
# constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
|
34
34
|
# <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
|
35
|
-
# as with the association objects,
|
36
|
-
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really
|
35
|
+
# as with the association objects, named \scopes act like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
|
36
|
+
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really was an Array.
|
37
37
|
#
|
38
|
-
# These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
|
38
|
+
# These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
|
39
39
|
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
|
40
40
|
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
|
41
41
|
#
|
42
|
-
# All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
|
42
|
+
# All \scopes are available as class methods on the ActiveRecord::Base descendent upon which the \scopes were defined. But they are also available to
|
43
43
|
# <tt>has_many</tt> associations. If,
|
44
44
|
#
|
45
45
|
# class Person < ActiveRecord::Base
|
@@ -49,7 +49,7 @@ module ActiveRecord
|
|
49
49
|
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
|
50
50
|
# only shirts.
|
51
51
|
#
|
52
|
-
# Named scopes can also be procedural
|
52
|
+
# Named \scopes can also be procedural:
|
53
53
|
#
|
54
54
|
# class Shirt < ActiveRecord::Base
|
55
55
|
# named_scope :colored, lambda { |color|
|
@@ -59,7 +59,7 @@ module ActiveRecord
|
|
59
59
|
#
|
60
60
|
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
|
61
61
|
#
|
62
|
-
# Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
|
62
|
+
# Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
|
63
63
|
#
|
64
64
|
# class Shirt < ActiveRecord::Base
|
65
65
|
# named_scope :red, :conditions => {:color => 'red'} do
|
@@ -70,7 +70,7 @@ module ActiveRecord
|
|
70
70
|
# end
|
71
71
|
#
|
72
72
|
#
|
73
|
-
# For testing complex named scopes, you can examine the scoping options using the
|
73
|
+
# For testing complex named \scopes, you can examine the scoping options using the
|
74
74
|
# <tt>proxy_options</tt> method on the proxy itself.
|
75
75
|
#
|
76
76
|
# class Shirt < ActiveRecord::Base
|
@@ -101,9 +101,9 @@ module ActiveRecord
|
|
101
101
|
|
102
102
|
class Scope
|
103
103
|
attr_reader :proxy_scope, :proxy_options
|
104
|
-
|
104
|
+
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
|
105
105
|
[].methods.each do |m|
|
106
|
-
unless m =~ /(
|
106
|
+
unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
|
107
107
|
delegate m, :to => :proxy_found
|
108
108
|
end
|
109
109
|
end
|
@@ -136,6 +136,10 @@ module ActiveRecord
|
|
136
136
|
end
|
137
137
|
end
|
138
138
|
|
139
|
+
def size
|
140
|
+
@found ? @found.length : count
|
141
|
+
end
|
142
|
+
|
139
143
|
def empty?
|
140
144
|
@found ? @found.empty? : count.zero?
|
141
145
|
end
|
@@ -144,6 +148,14 @@ module ActiveRecord
|
|
144
148
|
super || @proxy_scope.respond_to?(method, include_private)
|
145
149
|
end
|
146
150
|
|
151
|
+
def any?
|
152
|
+
if block_given?
|
153
|
+
proxy_found.any? { |*block_args| yield(*block_args) }
|
154
|
+
else
|
155
|
+
!empty?
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
147
159
|
protected
|
148
160
|
def proxy_found
|
149
161
|
@found || load_found
|
@@ -154,7 +166,8 @@ module ActiveRecord
|
|
154
166
|
if scopes.include?(method)
|
155
167
|
scopes[method].call(self, *args)
|
156
168
|
else
|
157
|
-
with_scope :find => proxy_options do
|
169
|
+
with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do
|
170
|
+
method = :new if method == :build
|
158
171
|
proxy_scope.send(method, *args, &block)
|
159
172
|
end
|
160
173
|
end
|